Skip to content

Commit

Permalink
chore(cos): include module and function on exit
Browse files Browse the repository at this point in the history
We use the result of a cached utility function to retrieve the function
object referencing a given code object. This allows us to retrieve the
module and the fully qualified function name for the frames on the stack
of the exit span.
  • Loading branch information
P403n1x87 committed Jan 9, 2025
1 parent 41fce6d commit 1f5a399
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 7 deletions.
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("_dd.code_origin.frames.0.type", f.__module__)
span.set_tag_str("_dd.code_origin.frames.0.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]

0 comments on commit 1f5a399

Please sign in to comment.