Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(cos): include module and function on exit #11882

Merged
merged 4 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions ddtrace/debugging/_function/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ddtrace.internal.module import origin
from ddtrace.internal.safety import _isinstance
from ddtrace.internal.utils.inspection import collect_code_objects
from ddtrace.internal.utils.inspection import functions_for_code
from ddtrace.internal.utils.inspection import linenos


Expand Down Expand Up @@ -141,13 +142,11 @@ def __init__(self, code: Optional[CodeType] = None, function: Optional[FunctionT
self.code = function.__code__ if function is not None else code

def resolve(self) -> FullyNamedFunction:
import gc

if self.function is not None:
return cast(FullyNamedFunction, self.function)

code = self.code
functions = [_ for _ in gc.get_referrers(code) if isinstance(_, FunctionType) and _.__code__ is code]
functions = functions_for_code(code)
n = len(functions)
if n == 0:
msg = f"Cannot resolve code object to function: {code}"
Expand Down Expand Up @@ -270,7 +269,11 @@ def __init__(self, module: ModuleType) -> None:
# If the module was already loaded we don't have its code object
seen_functions = set()
for _, fcp in self._fullname_index.items():
function = fcp.resolve()
try:
function = fcp.resolve()
except ValueError:
continue

if (
function not in seen_functions
and Path(cast(FunctionType, function).__code__.co_filename).resolve() == module_path
Expand Down Expand Up @@ -312,6 +315,8 @@ def by_name(self, qualname: str) -> FullyNamedFunction:
fullname = f"{self._module.__name__}.{qualname}"
try:
return self._fullname_index[fullname].resolve()
except ValueError:
pass
except KeyError:
if PYTHON_VERSION_INFO < (3, 11):
# Check if any code objects whose names match the last part of
Expand All @@ -336,7 +341,7 @@ def by_name(self, qualname: str) -> FullyNamedFunction:
return f
except ValueError:
pass
raise ValueError("Function '%s' not found" % fullname)
raise ValueError("Function '%s' not found" % fullname)

@classmethod
def from_module(cls, module: ModuleType) -> "FunctionDiscovery":
Expand Down
9 changes: 7 additions & 2 deletions ddtrace/debugging/_origin/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ddtrace.internal import core
from ddtrace.internal.packages import is_user_code
from ddtrace.internal.safety import _isinstance
from ddtrace.internal.utils.inspection import functions_for_code
from ddtrace.internal.wrapping.context import WrappingContext
from ddtrace.settings.code_origin import config as co_config
from ddtrace.span import Span
Expand Down Expand Up @@ -216,8 +217,12 @@ def on_span_start(self, span: Span) -> None:

span.set_tag_str(f"_dd.code_origin.frames.{n}.file", filename)
span.set_tag_str(f"_dd.code_origin.frames.{n}.line", str(code.co_firstlineno))
# DEV: Without a function object we cannot infer the function
# and any potential class name.
try:
(f,) = functions_for_code(code)
span.set_tag_str(f"_dd.code_origin.frames.{n}.type", f.__module__)
span.set_tag_str(f"_dd.code_origin.frames.{n}.method", f.__qualname__)
except ValueError:
continue

# TODO[gab]: This will be enabled as part of the live debugger/distributed debugging
# if ld_config.enabled:
Expand Down
9 changes: 9 additions & 0 deletions ddtrace/internal/utils/inspection.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from collections import deque
from dis import findlinestarts
from functools import lru_cache
from functools import partial
from functools import singledispatch
from pathlib import Path
from types import CodeType
from types import FunctionType
from typing import Iterator
from typing import List
from typing import Set
from typing import cast

Expand Down Expand Up @@ -122,3 +124,10 @@ def collect_code_objects(code: CodeType) -> Iterator[CodeType]:
for new_code in (_ for _ in c.co_consts if isinstance(_, CodeType)):
yield new_code
q.append(new_code)


@lru_cache()
def functions_for_code(code: CodeType) -> List[FunctionType]:
import gc

return [_ for _ in gc.get_referrers(code) if isinstance(_, FunctionType) and _.__code__ is code]
3 changes: 3 additions & 0 deletions tests/debugging/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import pytest

from ddtrace.internal.utils.inspection import functions_for_code


@pytest.fixture
def stuff():
functions_for_code.cache_clear()
was_loaded = False
if "tests.submod.stuff" in sys.modules:
was_loaded = True
Expand Down
48 changes: 17 additions & 31 deletions tests/debugging/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,9 +722,7 @@ def test_debugger_function_probe_duration(duration):
assert 0.9 * duration <= snapshot.duration <= 10.0 * duration, snapshot


def test_debugger_condition_eval_then_rate_limit():
from tests.submod.stuff import Stuff

def test_debugger_condition_eval_then_rate_limit(stuff):
with debugger(upload_flush_interval=float("inf")) as d:
d.add_probes(
create_snapshot_line_probe(
Expand All @@ -739,7 +737,7 @@ def test_debugger_condition_eval_then_rate_limit():
# before 42 won't use any of the probe quota. However, all the calls
# after 42 won't be snapshotted because of the rate limiter.
for i in range(100):
Stuff().instancestuff(i)
stuff.Stuff().instancestuff(i)

(snapshot,) = d.uploader.wait_for_payloads()

Expand All @@ -752,9 +750,7 @@ def test_debugger_condition_eval_then_rate_limit():
), snapshot


def test_debugger_condition_eval_error_get_reported_once():
from tests.submod.stuff import Stuff

def test_debugger_condition_eval_error_get_reported_once(stuff):
with debugger(upload_flush_interval=float("inf")) as d:
d.add_probes(
create_snapshot_line_probe(
Expand All @@ -767,7 +763,7 @@ def test_debugger_condition_eval_error_get_reported_once():

# all condition eval would fail
for i in range(100):
Stuff().instancestuff(i)
stuff.Stuff().instancestuff(i)

(snapshot,) = d.uploader.wait_for_payloads()

Expand All @@ -781,9 +777,7 @@ def test_debugger_condition_eval_error_get_reported_once():
assert "No such local variable: 'foo'" == evaluationErrors[0]["message"]


def test_debugger_function_probe_eval_on_entry():
from tests.submod.stuff import mutator

def test_debugger_function_probe_eval_on_entry(stuff):
with debugger() as d:
d.add_probes(
create_snapshot_function_probe(
Expand All @@ -797,7 +791,7 @@ def test_debugger_function_probe_eval_on_entry():
)
)

mutator(arg=[])
stuff.mutator(arg=[])

with d.assert_single_snapshot() as snapshot:
assert snapshot, d.test_queue
Expand All @@ -820,9 +814,7 @@ def test_debugger_run_module():
assert status == 0


def test_debugger_function_probe_eval_on_exit():
from tests.submod.stuff import mutator

def test_debugger_function_probe_eval_on_exit(stuff):
with debugger() as d:
d.add_probes(
create_snapshot_function_probe(
Expand All @@ -834,17 +826,15 @@ def test_debugger_function_probe_eval_on_exit():
)
)

mutator(arg=[])
stuff.mutator(arg=[])

with d.assert_single_snapshot() as snapshot:
assert snapshot, d.test_queue
assert not snapshot.entry_capture
assert 1 == snapshot.return_capture["arguments"]["arg"]["size"]


def test_debugger_lambda_fuction_access_locals():
from tests.submod.stuff import age_checker

def test_debugger_lambda_fuction_access_locals(stuff):
class Person(object):
def __init__(self, age, name):
self.age = age
Expand All @@ -866,10 +856,10 @@ def __init__(self, age, name):
)

# should capture as alice is in people list
age_checker(people=[Person(10, "alice"), Person(20, "bob"), Person(30, "charile")], age=18, name="alice")
stuff.age_checker(people=[Person(10, "alice"), Person(20, "bob"), Person(30, "charile")], age=18, name="alice")

# should skip as david is not in people list
age_checker(people=[Person(10, "alice"), Person(20, "bob"), Person(30, "charile")], age=18, name="david")
stuff.age_checker(people=[Person(10, "alice"), Person(20, "bob"), Person(30, "charile")], age=18, name="david")

assert d.signal_state_counter[SignalState.SKIP_COND] == 1
assert d.signal_state_counter[SignalState.DONE] == 1
Expand All @@ -878,9 +868,7 @@ def __init__(self, age, name):
assert snapshot, d.test_queue


def test_debugger_log_line_probe_generate_messages():
from tests.submod.stuff import Stuff

def test_debugger_log_line_probe_generate_messages(stuff):
with debugger(upload_flush_interval=float("inf")) as d:
d.add_probes(
create_log_line_probe(
Expand All @@ -898,8 +886,8 @@ def test_debugger_log_line_probe_generate_messages():
),
)

Stuff().instancestuff(123)
Stuff().instancestuff(456)
stuff.Stuff().instancestuff(123)
stuff.Stuff().instancestuff(456)

msg1, msg2 = d.uploader.wait_for_payloads(2)
assert "hello world ERROR 123!" == msg1["message"], msg1
Expand Down Expand Up @@ -1064,9 +1052,7 @@ def test_debugger_function_probe_ordering(self):
assert span.get_tag("test.tag") == "test.value"


def test_debugger_modified_probe():
from tests.submod.stuff import Stuff

def test_debugger_modified_probe(stuff):
with debugger(upload_flush_interval=float("inf")) as d:
d.add_probes(
create_log_line_probe(
Expand All @@ -1078,7 +1064,7 @@ def test_debugger_modified_probe():
)
)

Stuff().instancestuff()
stuff.Stuff().instancestuff()

(msg,) = d.uploader.wait_for_payloads()
assert "hello world" == msg["message"], msg
Expand All @@ -1094,7 +1080,7 @@ def test_debugger_modified_probe():
)
)

Stuff().instancestuff()
stuff.Stuff().instancestuff()

_, msg = d.uploader.wait_for_payloads(2)
assert "hello brave new world" == msg["message"], msg
Expand Down
3 changes: 3 additions & 0 deletions tests/debugging/test_debugger_span_decoration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ddtrace.debugging._probe.model import SpanDecorationTag
from ddtrace.debugging._probe.model import SpanDecorationTargetSpan
from ddtrace.debugging._signal.model import EvaluationError
from ddtrace.internal.utils.inspection import functions_for_code
from tests.debugging.mocking import debugger
from tests.debugging.utils import create_span_decoration_function_probe
from tests.debugging.utils import create_span_decoration_line_probe
Expand All @@ -21,6 +22,8 @@ def setUp(self):

import tests.submod.traced_stuff as ts

functions_for_code.cache_clear()

self.traced_stuff = ts
self.backup_tracer = ddtrace.tracer
self.old_traceme = ts.traceme
Expand Down
Loading