Skip to content
Draft
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
13 changes: 1 addition & 12 deletions ddtrace/debugging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ graph LR
## Code Origin for Spans

Code Origin for Spans is a product that allows retrieving code origin
information for exit and entry spans. The implementation for the two types of
span is different.
information for entry spans.

For **entry** spans, we listen for the `service_entrypoint.patch` core event,
which is emitted every time an integration is about to patch a service
Expand All @@ -141,20 +140,14 @@ the entrypoint is then instrumented with a wrapping context to allow the
extraction of code origin information (pre-computed and cached for performance),
as well as a snapshot, if required.

For **exit** spans, we register a span processor that performs the required work
when a span is created, provided the span kind is one that can be considered an
exit span (e.g. HTTP, DB etc...).

```mermaid
graph TD
subgraph "Code Origin for Spans"
SP[SpanCodeOriginProcessor]
WC[EntrySpanWrappingContext]
end

subgraph Tracer
EN[Entry span]
EX[Exit span]
I[Integration]
end

Expand All @@ -168,9 +161,5 @@ graph TD
WC -->|instrument code| IC
IC -->|attach/capture entry information| EN

EX -->|on span creation| SP
SP -->|attach/capture exit information| EX

WC -->|enqueue snapshots| U
SP -->|enqueue snapshots| U
```
136 changes: 0 additions & 136 deletions ddtrace/debugging/_origin/span.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from dataclasses import dataclass
from itertools import count
import sys
from threading import current_thread
from time import monotonic_ns
from types import FrameType
Expand All @@ -10,25 +8,19 @@
import uuid

import ddtrace
from ddtrace._trace.processor import SpanProcessor
from ddtrace.debugging._probe.model import DEFAULT_CAPTURE_LIMITS
from ddtrace.debugging._probe.model import LiteralTemplateSegment
from ddtrace.debugging._probe.model import LogFunctionProbe
from ddtrace.debugging._probe.model import LogLineProbe
from ddtrace.debugging._probe.model import ProbeEvalTiming
from ddtrace.debugging._session import Session
from ddtrace.debugging._signal.snapshot import Snapshot
from ddtrace.debugging._uploader import SignalUploader
from ddtrace.debugging._uploader import UploaderProduct
from ddtrace.ext import EXIT_SPAN_TYPES
from ddtrace.internal.compat import Path
from ddtrace.internal.forksafe import Lock
from ddtrace.internal.logger import get_logger
from ddtrace.internal.packages import is_user_code
from ddtrace.internal.safety import _isinstance
from ddtrace.internal.settings.code_origin import config as co_config
from ddtrace.internal.wrapping.context import WrappingContext
from ddtrace.trace import Span


log = get_logger(__name__)
Expand Down Expand Up @@ -66,42 +58,6 @@ def build(cls, name: str, module: str, function: str) -> "EntrySpanProbe":
)


@dataclass
class ExitSpanProbe(LogLineProbe):
__span_class__ = "exit"

@classmethod
def build(cls, name: str, filename: str, line: int) -> "ExitSpanProbe":
message = f"{cls.__span_class__} span info for {name}, in {filename}, at {line}"

return cls(
probe_id=str(uuid.uuid4()),
version=0,
tags={},
source_file=filename,
line=line,
template=message,
segments=[LiteralTemplateSegment(message)],
take_snapshot=True,
limits=DEFAULT_CAPTURE_LIMITS,
condition=None,
condition_error_rate=0.0,
rate=float("inf"),
)

@classmethod
def from_frame(cls, frame: FrameType) -> "ExitSpanProbe":
code = frame.f_code
return t.cast(
ExitSpanProbe,
cls.build(
name=code.co_qualname if sys.version_info >= (3, 11) else code.co_name, # type: ignore[attr-defined]
filename=str(Path(code.co_filename)),
line=frame.f_lineno,
),
)


@dataclass
class EntrySpanLocation:
name: str
Expand Down Expand Up @@ -264,95 +220,3 @@ def disable(cls):
cls._instance = None

log.debug("Code Origin for Spans (entry) disabled")


class SpanCodeOriginProcessorExit(SpanProcessor):
__uploader__ = SignalUploader

_instance: t.Optional["SpanCodeOriginProcessorExit"] = None

def on_span_start(self, span: Span) -> None:
if span.span_type not in EXIT_SPAN_TYPES:
return

span._set_tag_str("_dd.code_origin.type", "exit")

# Add call stack information to the exit span. Report only the part of
# the stack that belongs to user code.
seq = count(0)
for frame in frame_stack(sys._getframe(1)):
code = frame.f_code
filename = code.co_filename

if is_user_code(filename):
n = next(seq)
if n >= co_config.max_user_frames:
break

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))

# Get the module and function name from the frame and code object. In Python3.11+ qualname
# is available, otherwise we'll fallback to the unqualified name.
try:
name = code.co_qualname # type: ignore[attr-defined]
except AttributeError:
name = code.co_name

mod = frame.f_globals.get("__name__")
span._set_tag_str(f"_dd.code_origin.frames.{n}.type", mod) if mod else None
span._set_tag_str(f"_dd.code_origin.frames.{n}.method", name) if name else None

# Check if we have any level 2 debugging sessions running for
# the current trace
if any(s.level >= 2 for s in Session.from_trace(span.context)):
# Create a snapshot
snapshot = Snapshot(
probe=ExitSpanProbe.from_frame(frame),
frame=frame,
thread=current_thread(),
trace_context=span,
)

# Capture on entry
snapshot.do_line()

# Collect
if (collector := self.__uploader__.get_collector()) is not None:
collector.push(snapshot)

# Correlate the snapshot with the span
span._set_tag_str(f"_dd.code_origin.frames.{n}.snapshot_id", snapshot.uuid)

def on_span_finish(self, span: Span) -> None:
pass

@classmethod
def enable(cls):
if cls._instance is not None:
return

instance = cls._instance = cls()

# Register code origin for span with the snapshot uploader
cls.__uploader__.register(UploaderProduct.CODE_ORIGIN_SPAN_EXIT)

# Register the processor for exit spans
instance.register()

log.debug("Code Origin for Spans (exit) enabled")

@classmethod
def disable(cls):
if cls._instance is None:
return

# Unregister the processor for exit spans
cls._instance.unregister()

# Unregister code origin for span with the snapshot uploader
cls.__uploader__.unregister(UploaderProduct.CODE_ORIGIN_SPAN_EXIT)

cls._instance = None

log.debug("Code Origin for Spans (exit) disabled")
24 changes: 6 additions & 18 deletions ddtrace/debugging/_products/code_origin/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,10 @@ def _(f: t.Union[FunctionType, MethodType]) -> None:

log.debug("Registered entrypoint patching hook for code origin for spans")

if config.span.enabled:
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit

SpanCodeOriginProcessorExit.enable()

_start()
# If dynamic instrumentation is enabled, and code origin for spans is not explicitly disabled,
# we'll enable entry spans only.
elif product_manager.is_enabled(DI_PRODUCT_KEY) and config.value_source(CO_ENABLED) == ValueSource.DEFAULT:
# we'll enable code origin for spans.
di_enabled = product_manager.is_enabled(DI_PRODUCT_KEY) and config.value_source(CO_ENABLED) == ValueSource.DEFAULT
if config.span.enabled or di_enabled:
_start()


Expand All @@ -64,16 +59,9 @@ def _stop():


def stop(join=False):
if config.span.enabled:
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit

SpanCodeOriginProcessorEntry.disable()
SpanCodeOriginProcessorExit.disable()
elif product_manager.is_enabled(DI_PRODUCT_KEY):
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry

SpanCodeOriginProcessorEntry.disable()
di_enabled = product_manager.is_enabled(DI_PRODUCT_KEY) and config.value_source(CO_ENABLED) == ValueSource.DEFAULT
if config.span.enabled or di_enabled:
_stop()


def at_exit(join=False):
Expand Down
1 change: 0 additions & 1 deletion ddtrace/debugging/_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class UploaderProduct(str, Enum):
DEBUGGER = "dynamic_instrumentation"
EXCEPTION_REPLAY = "exception_replay"
CODE_ORIGIN_SPAN_ENTRY = "code_origin.span.entry"
CODE_ORIGIN_SPAN_EXIT = "code_origin.span.exit"


@dataclass
Expand Down
15 changes: 0 additions & 15 deletions ddtrace/ext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,3 @@ class SpanKind(object):
class SpanLinkKind(object):
EXECUTED = "executed_by"
RESUMING = "resuming"


EXIT_SPAN_TYPES = frozenset(
{
SpanTypes.CACHE,
SpanTypes.CASSANDRA,
SpanTypes.ELASTICSEARCH,
SpanTypes.GRPC,
SpanTypes.HTTP,
SpanTypes.REDIS,
SpanTypes.SQL,
SpanTypes.WORKER,
SpanTypes.VALKEY,
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
prelude: >
**Breaking change** for Code Origin for Spans: Outgoing requests are no longer included with code origin for spans.
fixes:
- |
Code Origin for Spans: Outgoing requests are no longer included with code origin for spans.
4 changes: 2 additions & 2 deletions tests/debugging/live/test_live_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import typing as t

import ddtrace
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
from ddtrace.debugging._probe.model import ProbeEvalTiming
from ddtrace.internal import core
from tests.debugging.mocking import MockSignalUploader
Expand All @@ -12,7 +12,7 @@
from tests.utils import TracerTestCase


class MockSpanCodeOriginProcessor(SpanCodeOriginProcessorExit):
class MockSpanCodeOriginProcessor(SpanCodeOriginProcessorEntry):
__uploader__ = MockSignalUploader

@classmethod
Expand Down
43 changes: 10 additions & 33 deletions tests/debugging/origin/test_span.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import ddtrace
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit
from ddtrace.debugging._session import Session
from ddtrace.ext import SpanTypes
from ddtrace.internal import core
Expand All @@ -30,35 +29,19 @@ def get_uploader(cls) -> MockSignalUploader:
return t.cast(MockSignalUploader, cls.__uploader__._instance)


class MockSpanCodeOriginProcessor(SpanCodeOriginProcessorExit):
__uploader__ = MockSignalUploader

@classmethod
def get_uploader(cls) -> MockSignalUploader:
return t.cast(MockSignalUploader, cls.__uploader__._instance)


class SpanProbeTestCase(TracerTestCase):
def setUp(self):
super(SpanProbeTestCase, self).setUp()
self.backup_tracer = ddtrace.tracer
ddtrace.tracer = self.tracer

MockSpanCodeOriginProcessorEntry.enable()
MockSpanCodeOriginProcessor.enable()

def tearDown(self):
ddtrace.tracer = self.backup_tracer
super(SpanProbeTestCase, self).tearDown()

if (uploader := MockSpanCodeOriginProcessor.get_uploader()) is not None:
uploader.flush()

MockSpanCodeOriginProcessorEntry.disable()
MockSpanCodeOriginProcessor.disable()

assert MockSpanCodeOriginProcessor.get_uploader() is None

core.reset_listeners(event_id="service_entrypoint.patch")

def test_span_origin(self):
Expand Down Expand Up @@ -89,10 +72,10 @@ def entry_call():
assert middle.get_tag("_dd.code_origin.frames.0.file") is None
assert middle.get_tag("_dd.code_origin.frames.0.file") is None

# Check for the expected tags on the exit span
assert _exit.get_tag("_dd.code_origin.type") == "exit"
assert _exit.get_tag("_dd.code_origin.frames.0.file") == str(Path(__file__).resolve())
assert _exit.get_tag("_dd.code_origin.frames.0.line") == str(self.test_span_origin.__code__.co_firstlineno)
# Check that we also don't have the span location tags on the exit span
assert _exit.get_tag("_dd.code_origin.type") is None
assert _exit.get_tag("_dd.code_origin.frames.0.file") is None
assert _exit.get_tag("_dd.code_origin.frames.0.line") is None

@pytest.mark.skip(reason="Frequent unreliable failures")
def test_span_origin_session(self):
Expand All @@ -111,7 +94,7 @@ def entry_call():

self.assert_span_count(3)
entry, middle, _exit = self.get_spans()
payloads = MockSpanCodeOriginProcessor.get_uploader().wait_for_payloads()
payloads = MockSpanCodeOriginProcessorEntry.get_uploader().wait_for_payloads()
snapshot_ids = {p["debugger"]["snapshot"]["id"] for p in payloads}

assert len(payloads) == len(snapshot_ids)
Expand All @@ -123,20 +106,14 @@ def entry_call():
# Check that we don't have span location tags on the middle span
assert middle.get_tag("_dd.code_origin.frames.0.snapshot_id") is None

# Check that we have all the snapshots for the exit span
assert _exit.get_tag("_dd.code_origin.type") == "exit"
snapshot_ids_from_span_tags = {_exit.get_tag(f"_dd.code_origin.frames.{_}.snapshot_id") for _ in range(8)}
snapshot_ids_from_span_tags.discard(None)
assert snapshot_ids_from_span_tags < snapshot_ids
# Check that we don't have span location tags on the exit span
assert _exit.get_tag("_dd.code_origin.type") is None
assert _exit.get_tag("_dd.code_origin.frames.0.snapshot_id") is None

# Check that we have complete data
snapshot_ids_from_span_tags.add(entry_snapshot_id)
assert snapshot_ids_from_span_tags == snapshot_ids
# Check that we only have the entry snapshot
assert snapshot_ids == {entry_snapshot_id}

def test_span_origin_entry(self):
# Disable the processor to avoid interference with the test
MockSpanCodeOriginProcessor.disable()

def entry_call():
pass

Expand Down
Loading