Skip to content

Commit 5658801

Browse files
NiteshDhanpalclaude
andcommitted
fix(tracing): nest ddtrace wrapper under the active context (child_of)
ddtrace start_span does not auto-parent (unlike OTel): start_span(name) mints a new ROOT trace every call, so a turn's business spans scattered across N Datadog traces (verified live: 52 spans -> 52 distinct obs_trace_ids). Pass child_of=current_trace_context() so wrappers nest under the request/turn trace and roll up into one trace; obs_span_id stays distinct per step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 637d74f commit 5658801

2 files changed

Lines changed: 19 additions & 6 deletions

File tree

src/agentex/lib/core/tracing/obs_span.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,15 @@ def _open_ddtrace_span(
116116
try:
117117
# Only wrap when ddtrace is actually tracing the request; otherwise a
118118
# wrapper would be an orphan root trace in an un-instrumented process.
119-
if tracer.current_trace_context() is None:
119+
ctx = tracer.current_trace_context()
120+
if ctx is None:
120121
return None
121-
span = tracer.start_span(name, activate=True)
122+
# child_of=ctx is load-bearing: ddtrace's start_span does NOT auto-parent
123+
# to the active span (unlike OTel), so start_span(name) alone mints a NEW
124+
# root trace every call -- scattering a turn's business spans across N
125+
# Datadog traces. Parenting to the active request/turn context rolls them
126+
# into one trace while obs_span_id stays distinct per step.
127+
span = tracer.start_span(name, child_of=ctx, activate=True)
122128
if business_span_id:
123129
span.set_tag(_ATTR_BUSINESS_SPAN_ID, business_span_id)
124130
if business_trace_id:

tests/lib/core/tracing/test_obs_span.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,17 @@ def finish(self):
9595

9696
def _install_fake_ddtrace(monkeypatch, *, active=True, trace_id=0xABC, span_id=0xFF):
9797
record: dict = {"span": None, "started": []}
98+
ctx_obj = object() if active else None
99+
record["ctx"] = ctx_obj
98100

99-
def start_span(name, activate=False):
101+
def start_span(name, child_of=None, activate=False):
100102
span = _FakeDDSpan(name, trace_id, span_id)
101103
record["span"] = span
102-
record["started"].append((name, activate))
104+
record["started"].append({"name": name, "child_of": child_of, "activate": activate})
103105
return span
104106

105107
tracer = types.SimpleNamespace(
106-
current_trace_context=lambda: (object() if active else None),
108+
current_trace_context=lambda: ctx_obj,
107109
start_span=start_span,
108110
)
109111
fake_ddtrace = types.ModuleType("ddtrace")
@@ -204,7 +206,12 @@ def test_dd_only_with_active_ctx_opens_named_span(self, monkeypatch):
204206

205207
assert handle is not None
206208
assert record["span"].name == "rocket.tool.fetch"
207-
assert record["started"] == [("rocket.tool.fetch", True)] # activated
209+
started = record["started"][0]
210+
assert started["name"] == "rocket.tool.fetch"
211+
assert started["activate"] is True
212+
# child_of is the active request/turn context -> the wrapper nests under
213+
# it instead of minting a new root trace (ddtrace does not auto-parent).
214+
assert started["child_of"] is record["ctx"]
208215
assert handle.correlation == {
209216
"obs_trace_id": "00000000000000000000000000000abc",
210217
"obs_span_id": "000000000000000000ff"[-16:],

0 commit comments

Comments
 (0)