diff --git a/ddtrace/debugging/_function/discovery.py b/ddtrace/debugging/_function/discovery.py index 6a259f0f93c..1c32712d6ba 100644 --- a/ddtrace/debugging/_function/discovery.py +++ b/ddtrace/debugging/_function/discovery.py @@ -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 @@ -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}" @@ -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 @@ -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 @@ -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": diff --git a/ddtrace/debugging/_origin/span.py b/ddtrace/debugging/_origin/span.py index 9b592df2bde..e7831c38ffb 100644 --- a/ddtrace/debugging/_origin/span.py +++ b/ddtrace/debugging/_origin/span.py @@ -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 @@ -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: diff --git a/ddtrace/internal/utils/inspection.py b/ddtrace/internal/utils/inspection.py index bb24a3ae80d..72da78c5e3e 100644 --- a/ddtrace/internal/utils/inspection.py +++ b/ddtrace/internal/utils/inspection.py @@ -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 @@ -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] diff --git a/tests/debugging/conftest.py b/tests/debugging/conftest.py index 71664983ea4..862f817a055 100644 --- a/tests/debugging/conftest.py +++ b/tests/debugging/conftest.py @@ -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 diff --git a/tests/debugging/test_debugger.py b/tests/debugging/test_debugger.py index 0cc65bc43cf..bacfcbcdd45 100644 --- a/tests/debugging/test_debugger.py +++ b/tests/debugging/test_debugger.py @@ -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( @@ -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() @@ -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( @@ -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() @@ -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( @@ -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 @@ -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( @@ -834,7 +826,7 @@ 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 @@ -842,9 +834,7 @@ def test_debugger_function_probe_eval_on_exit(): 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 @@ -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 @@ -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( @@ -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 @@ -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( @@ -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 @@ -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 diff --git a/tests/debugging/test_debugger_span_decoration.py b/tests/debugging/test_debugger_span_decoration.py index 5d7e51ee6a7..457f6b3a919 100644 --- a/tests/debugging/test_debugger_span_decoration.py +++ b/tests/debugging/test_debugger_span_decoration.py @@ -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 @@ -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