Skip to content

Commit 1b249e1

Browse files
authored
feat(di): enable code origins for entry spans when di is enabled (#13593)
Enable code origins for entry spans when dynamic instrumentation is enabled. This also breaks up code origin entry and exit spans into separate classes. This include a new processor for entry spans, updates to configuration and product management, and additional tests to ensure functionality and coverage. Refs: DEBUG-3787
1 parent 3ff7046 commit 1b249e1

File tree

8 files changed

+154
-29
lines changed

8 files changed

+154
-29
lines changed

ddtrace/debugging/_origin/span.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,46 @@ def __exit__(self, exc_type, exc_value, traceback):
196196

197197

198198
@dataclass
199-
class SpanCodeOriginProcessor(SpanProcessor):
199+
class SpanCodeOriginProcessorEntry:
200200
__uploader__ = LogsIntakeUploaderV1
201201

202-
_instance: t.Optional["SpanCodeOriginProcessor"] = None
202+
_instance: t.Optional["SpanCodeOriginProcessorEntry"] = None
203203
_handler: t.Optional[t.Callable] = None
204204

205+
@classmethod
206+
def enable(cls):
207+
if cls._instance is not None:
208+
return
209+
210+
cls._instance = cls()
211+
212+
# Register code origin for span with the snapshot uploader
213+
cls.__uploader__.register(UploaderProduct.CODE_ORIGIN_SPAN)
214+
215+
# Register the entrypoint wrapping for entry spans
216+
cls._handler = handler = partial(wrap_entrypoint, cls.__uploader__.get_collector())
217+
core.on("service_entrypoint.patch", handler)
218+
219+
@classmethod
220+
def disable(cls):
221+
if cls._instance is None:
222+
return
223+
224+
# Unregister the entrypoint wrapping for entry spans
225+
core.reset_listeners("service_entrypoint.patch", cls._handler)
226+
# Unregister code origin for span with the snapshot uploader
227+
cls.__uploader__.unregister(UploaderProduct.CODE_ORIGIN_SPAN)
228+
229+
cls._handler = None
230+
cls._instance = None
231+
232+
233+
@dataclass
234+
class SpanCodeOriginProcessorExit(SpanProcessor):
235+
__uploader__ = LogsIntakeUploaderV1
236+
237+
_instance: t.Optional["SpanCodeOriginProcessorExit"] = None
238+
205239
def on_span_start(self, span: Span) -> None:
206240
if span.span_type not in EXIT_SPAN_TYPES:
207241
return
@@ -270,19 +304,11 @@ def enable(cls):
270304
# Register the processor for exit spans
271305
instance.register()
272306

273-
# Register the entrypoint wrapping for entry spans
274-
cls._handler = handler = partial(wrap_entrypoint, cls.__uploader__.get_collector())
275-
core.on("service_entrypoint.patch", handler)
276-
277307
@classmethod
278308
def disable(cls):
279309
if cls._instance is None:
280310
return
281311

282-
# Unregister the entrypoint wrapping for entry spans
283-
core.reset_listeners("service_entrypoint.patch", cls._handler)
284-
cls._handler = None
285-
286312
# Unregister the processor for exit spans
287313
cls._instance.unregister()
288314

ddtrace/debugging/_products/code_origin/span.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
from ddtrace.internal.products import manager as product_manager
2+
from ddtrace.settings._core import ValueSource
13
from ddtrace.settings.code_origin import config
24

35

6+
CO_ENABLED = "DD_CODE_ORIGIN_FOR_SPANS_ENABLED"
7+
DI_PRODUCT_KEY = "dynamic-instrumentation"
8+
49
# TODO[gab]: Uncomment this when the feature is ready
510
# requires = ["tracer"]
611

@@ -11,9 +16,17 @@ def post_preload():
1116

1217
def start():
1318
if config.span.enabled:
14-
from ddtrace.debugging._origin.span import SpanCodeOriginProcessor
19+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
20+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit
21+
22+
SpanCodeOriginProcessorEntry.enable()
23+
SpanCodeOriginProcessorExit.enable()
24+
# If dynamic instrumentation is enabled, and code origin for spans is not explicitly disabled,
25+
# we'll enable entry spans only.
26+
elif product_manager.is_enabled(DI_PRODUCT_KEY) and config.value_source(CO_ENABLED) == ValueSource.DEFAULT:
27+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
1528

16-
SpanCodeOriginProcessor.enable()
29+
SpanCodeOriginProcessorEntry.enable()
1730

1831

1932
def restart(join=False):
@@ -22,9 +35,15 @@ def restart(join=False):
2235

2336
def stop(join=False):
2437
if config.span.enabled:
25-
from ddtrace.debugging._origin.span import SpanCodeOriginProcessor
38+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
39+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit
40+
41+
SpanCodeOriginProcessorEntry.disable()
42+
SpanCodeOriginProcessorExit.disable()
43+
elif product_manager.is_enabled(DI_PRODUCT_KEY):
44+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
2645

27-
SpanCodeOriginProcessor.disable()
46+
SpanCodeOriginProcessorEntry.disable()
2847

2948

3049
def at_exit(join=False):

ddtrace/internal/products.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,5 +228,14 @@ def _() -> None:
228228
else:
229229
self._do_products()
230230

231+
def is_enabled(self, product_name: str, enabled_attribute: str = "enabled") -> bool:
232+
if (product := self.__products__.get(product_name)) is None:
233+
return False
234+
235+
if (config := getattr(product, "config", None)) is None:
236+
return False
237+
238+
return getattr(config, enabled_attribute, False)
239+
231240

232241
manager = ProductManager()

ddtrace/internal/symbol_db/remoteconfig.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,9 @@
1414
from ddtrace.internal.symbol_db.symbols import SymbolDatabaseUploader
1515

1616

17-
log = get_logger(__name__)
18-
19-
20-
def is_di_enabled() -> bool:
21-
if (di_product := product_manager.__products__.get("dynamic-instrumentation")) is None:
22-
return False
17+
DI_PRODUCT_KEY = "dynamic-instrumentation"
2318

24-
if (di_config := getattr(di_product, "config", None)) is None:
25-
return False
26-
27-
return di_config.enabled
19+
log = get_logger(__name__)
2820

2921

3022
def _rc_callback(data: t.List[Payload], test_tracer=None):
@@ -57,7 +49,7 @@ def _rc_callback(data: t.List[Payload], test_tracer=None):
5749
log.debug("[PID %d] SymDB: Symbol DB RCM enablement signal received", os.getpid())
5850
if not SymbolDatabaseUploader.is_installed():
5951
try:
60-
SymbolDatabaseUploader.install(shallow=not is_di_enabled())
52+
SymbolDatabaseUploader.install(shallow=not product_manager.is_enabled(DI_PRODUCT_KEY))
6153
log.debug("[PID %d] SymDB: Symbol DB uploader installed", os.getpid())
6254
except Exception:
6355
log.error("[PID %d] SymDB: Failed to install Symbol DB uploader", os.getpid(), exc_info=True)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
dynamic instrumentation: [Code Origins for Spans](https://docs.datadoghq.com/tracing/code_origins/) is now
5+
automatically enabled when [Dynamic Instrumentation](https://docs.datadoghq.com/dynamic_instrumentation/) is turned
6+
on.

tests/debugging/live/test_live_debugger.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import typing as t
33

44
import ddtrace
5-
from ddtrace.debugging._origin.span import SpanCodeOriginProcessor
5+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit
66
from ddtrace.debugging._probe.model import ProbeEvalTiming
77
from ddtrace.internal import core
88
from tests.debugging.mocking import MockLogsIntakeUploaderV1
@@ -12,7 +12,7 @@
1212
from tests.utils import TracerTestCase
1313

1414

15-
class MockSpanCodeOriginProcessor(SpanCodeOriginProcessor):
15+
class MockSpanCodeOriginProcessor(SpanCodeOriginProcessorExit):
1616
__uploader__ = MockLogsIntakeUploaderV1
1717

1818
@classmethod

tests/debugging/origin/test_span.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@
22
import typing as t
33

44
import ddtrace
5-
from ddtrace.debugging._origin.span import SpanCodeOriginProcessor
5+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry
6+
from ddtrace.debugging._origin.span import SpanCodeOriginProcessorExit
67
from ddtrace.debugging._session import Session
78
from ddtrace.ext import SpanTypes
89
from ddtrace.internal import core
910
from tests.debugging.mocking import MockLogsIntakeUploaderV1
1011
from tests.utils import TracerTestCase
1112

1213

13-
class MockSpanCodeOriginProcessor(SpanCodeOriginProcessor):
14+
class MockSpanCodeOriginProcessorEntry(SpanCodeOriginProcessorEntry):
15+
__uploader__ = MockLogsIntakeUploaderV1
16+
17+
@classmethod
18+
def get_uploader(cls) -> MockLogsIntakeUploaderV1:
19+
return t.cast(MockLogsIntakeUploaderV1, cls.__uploader__._instance)
20+
21+
22+
class MockSpanCodeOriginProcessor(SpanCodeOriginProcessorExit):
1423
__uploader__ = MockLogsIntakeUploaderV1
1524

1625
@classmethod
@@ -24,12 +33,14 @@ def setUp(self):
2433
self.backup_tracer = ddtrace.tracer
2534
ddtrace.tracer = self.tracer
2635

36+
MockSpanCodeOriginProcessorEntry.enable()
2737
MockSpanCodeOriginProcessor.enable()
2838

2939
def tearDown(self):
3040
ddtrace.tracer = self.backup_tracer
3141
super(SpanProbeTestCase, self).tearDown()
3242

43+
MockSpanCodeOriginProcessorEntry.disable()
3344
MockSpanCodeOriginProcessor.disable()
3445
core.reset_listeners(event_id="service_entrypoint.patch")
3546

@@ -103,3 +114,40 @@ def entry_call():
103114
# Check that we have complete data
104115
snapshot_ids_from_span_tags.add(entry_snapshot_id)
105116
assert snapshot_ids_from_span_tags == snapshot_ids
117+
118+
def test_span_origin_entry(self):
119+
# Disable the processor to avoid interference with the test
120+
MockSpanCodeOriginProcessor.disable()
121+
122+
def entry_call():
123+
pass
124+
125+
core.dispatch("service_entrypoint.patch", (entry_call,))
126+
127+
with self.tracer.trace("entry"):
128+
entry_call()
129+
with self.tracer.trace("middle"):
130+
with self.tracer.trace("exit", span_type=SpanTypes.HTTP):
131+
pass
132+
133+
self.assert_span_count(3)
134+
entry, middle, _exit = self.get_spans()
135+
136+
# Check for the expected tags on the entry span
137+
assert entry.get_tag("_dd.code_origin.type") == "entry"
138+
assert entry.get_tag("_dd.code_origin.frames.0.file") == str(Path(__file__).resolve())
139+
assert entry.get_tag("_dd.code_origin.frames.0.line") == str(entry_call.__code__.co_firstlineno)
140+
assert entry.get_tag("_dd.code_origin.frames.0.type") == __name__
141+
assert (
142+
entry.get_tag("_dd.code_origin.frames.0.method")
143+
== "SpanProbeTestCase.test_span_origin_entry.<locals>.entry_call"
144+
)
145+
146+
# Check that we don't have span location tags on the middle span
147+
assert middle.get_tag("_dd.code_origin.frames.0.file") is None
148+
assert middle.get_tag("_dd.code_origin.frames.0.file") is None
149+
150+
# Check that we also don't have the span location tags on the exit span
151+
assert _exit.get_tag("_dd.code_origin.type") is None
152+
assert _exit.get_tag("_dd.code_origin.frames.0.file") is None
153+
assert _exit.get_tag("_dd.code_origin.frames.0.line") is None

tests/internal/test_products.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,28 @@ def test_product_manager_restart():
113113
os._exit(0)
114114

115115
os.waitpid(pid, 0)
116+
117+
118+
def test_product_manager_is_enabled():
119+
class ProductWithConfig:
120+
def __init__(self, enabled):
121+
self.config = type("Config", (), {"enabled": enabled})()
122+
123+
# Test when product doesn't exist
124+
manager = ProductManagerTest({})
125+
assert not manager.is_enabled("nonexistent")
126+
127+
# Test when product exists but has no config
128+
product_no_config = BaseProduct()
129+
manager = ProductManagerTest({"no_config": product_no_config})
130+
assert not manager.is_enabled("no_config")
131+
132+
# Test when product exists and is enabled
133+
enabled_product = ProductWithConfig(True)
134+
manager = ProductManagerTest({"enabled": enabled_product})
135+
assert manager.is_enabled("enabled")
136+
137+
# Test when product exists but is disabled
138+
disabled_product = ProductWithConfig(False)
139+
manager = ProductManagerTest({"disabled": disabled_product})
140+
assert not manager.is_enabled("disabled")

0 commit comments

Comments
 (0)