diff --git a/ddtrace/debugging/README.md b/ddtrace/debugging/README.md index 25dcae8077a..a35a6d008cc 100644 --- a/ddtrace/debugging/README.md +++ b/ddtrace/debugging/README.md @@ -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 @@ -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 @@ -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 ``` diff --git a/ddtrace/debugging/_origin/span.py b/ddtrace/debugging/_origin/span.py index 8c0acb49c9f..033d4b87891 100644 --- a/ddtrace/debugging/_origin/span.py +++ b/ddtrace/debugging/_origin/span.py @@ -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 @@ -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__) @@ -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 @@ -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") diff --git a/ddtrace/debugging/_products/code_origin/span.py b/ddtrace/debugging/_products/code_origin/span.py index 4f6e89042fd..0e3ff65c1de 100644 --- a/ddtrace/debugging/_products/code_origin/span.py +++ b/ddtrace/debugging/_products/code_origin/span.py @@ -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() @@ -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): diff --git a/ddtrace/debugging/_uploader.py b/ddtrace/debugging/_uploader.py index e2e8683af44..37fcbf0ab24 100644 --- a/ddtrace/debugging/_uploader.py +++ b/ddtrace/debugging/_uploader.py @@ -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 diff --git a/ddtrace/ext/__init__.py b/ddtrace/ext/__init__.py index 1681018167c..b18c0740556 100644 --- a/ddtrace/ext/__init__.py +++ b/ddtrace/ext/__init__.py @@ -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, - } -) diff --git a/releasenotes/notes/disable-code-origin-exit-spans-86a0cb0d1619753f.yaml b/releasenotes/notes/disable-code-origin-exit-spans-86a0cb0d1619753f.yaml new file mode 100644 index 00000000000..96b005de548 --- /dev/null +++ b/releasenotes/notes/disable-code-origin-exit-spans-86a0cb0d1619753f.yaml @@ -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. diff --git a/tests/debugging/live/test_live_debugger.py b/tests/debugging/live/test_live_debugger.py index 4b97c6476b4..3f14f8bee54 100644 --- a/tests/debugging/live/test_live_debugger.py +++ b/tests/debugging/live/test_live_debugger.py @@ -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 @@ -12,7 +12,7 @@ from tests.utils import TracerTestCase -class MockSpanCodeOriginProcessor(SpanCodeOriginProcessorExit): +class MockSpanCodeOriginProcessor(SpanCodeOriginProcessorEntry): __uploader__ = MockSignalUploader @classmethod diff --git a/tests/debugging/origin/test_span.py b/tests/debugging/origin/test_span.py index 92831f528e9..fee287f3652 100644 --- a/tests/debugging/origin/test_span.py +++ b/tests/debugging/origin/test_span.py @@ -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 @@ -30,14 +29,6 @@ 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() @@ -45,20 +36,12 @@ def setUp(self): 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): @@ -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): @@ -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) @@ -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