Skip to content

Commit 1cea241

Browse files
authored
fix(tracing): propagate trace metadata to spans for processors (#2478)
1 parent 0c5698b commit 1cea241

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

src/agents/tracing/provider.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ def create_span(
309309
"""
310310
self._refresh_disabled_flag()
311311
tracing_api_key: str | None = None
312+
trace_metadata: dict[str, Any] | None = None
312313
if self._disabled or disabled:
313314
logger.debug(f"Tracing is disabled. Not creating span {span_data}")
314315
return NoOpSpan(span_data)
@@ -331,6 +332,8 @@ def create_span(
331332
parent_id = current_span.span_id if current_span else None
332333
trace_id = current_trace.trace_id
333334
tracing_api_key = current_trace.tracing_api_key
335+
# Trace is an interface; custom implementations may omit metadata.
336+
trace_metadata = getattr(current_trace, "metadata", None)
334337

335338
elif isinstance(parent, Trace):
336339
if isinstance(parent, NoOpTrace):
@@ -339,13 +342,16 @@ def create_span(
339342
trace_id = parent.trace_id
340343
parent_id = None
341344
tracing_api_key = parent.tracing_api_key
345+
# Trace is an interface; custom implementations may omit metadata.
346+
trace_metadata = getattr(parent, "metadata", None)
342347
elif isinstance(parent, Span):
343348
if isinstance(parent, NoOpSpan):
344349
logger.debug(f"Parent {parent} is no-op, returning NoOpSpan")
345350
return NoOpSpan(span_data)
346351
parent_id = parent.span_id
347352
trace_id = parent.trace_id
348353
tracing_api_key = parent.tracing_api_key
354+
trace_metadata = parent.trace_metadata
349355

350356
logger.debug(f"Creating span {span_data} with id {span_id}")
351357

@@ -356,6 +362,7 @@ def create_span(
356362
processor=self._multi_processor,
357363
span_data=span_data,
358364
tracing_api_key=tracing_api_key,
365+
trace_metadata=trace_metadata,
359366
)
360367

361368
def shutdown(self) -> None:

src/agents/tracing/spans.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ def tracing_api_key(self) -> str | None:
178178
"""The API key to use when exporting this span."""
179179
pass
180180

181+
@property
182+
def trace_metadata(self) -> dict[str, Any] | None:
183+
"""Trace-level metadata inherited by this span, if available."""
184+
return None
185+
181186

182187
class NoOpSpan(Span[TSpanData]):
183188
"""A no-op implementation of Span that doesn't record any data.
@@ -266,6 +271,7 @@ class SpanImpl(Span[TSpanData]):
266271
"_processor",
267272
"_span_data",
268273
"_tracing_api_key",
274+
"_trace_metadata",
269275
)
270276

271277
def __init__(
@@ -276,6 +282,7 @@ def __init__(
276282
processor: TracingProcessor,
277283
span_data: TSpanData,
278284
tracing_api_key: str | None,
285+
trace_metadata: dict[str, Any] | None = None,
279286
):
280287
self._trace_id = trace_id
281288
self._span_id = span_id or util.gen_span_id()
@@ -287,6 +294,7 @@ def __init__(
287294
self._prev_span_token: contextvars.Token[Span[TSpanData] | None] | None = None
288295
self._span_data = span_data
289296
self._tracing_api_key = tracing_api_key
297+
self._trace_metadata = trace_metadata
290298

291299
@property
292300
def trace_id(self) -> str:
@@ -356,6 +364,10 @@ def ended_at(self) -> str | None:
356364
def tracing_api_key(self) -> str | None:
357365
return self._tracing_api_key
358366

367+
@property
368+
def trace_metadata(self) -> dict[str, Any] | None:
369+
return self._trace_metadata
370+
359371
def export(self) -> dict[str, Any] | None:
360372
return {
361373
"object": "trace.span",

tests/test_tracing.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
from agents.tracing import (
1010
Span,
1111
Trace,
12+
TracingProcessor,
1213
agent_span,
1314
custom_span,
1415
function_span,
1516
generation_span,
1617
handoff_span,
18+
set_trace_processors,
1719
trace,
1820
)
1921
from agents.tracing.spans import SpanError
@@ -410,6 +412,64 @@ def test_trace_and_spans_use_tracing_config_key():
410412
assert span.tracing_api_key == "tracing-key"
411413

412414

415+
def test_trace_metadata_propagates_to_spans():
416+
metadata = {"source": "run"}
417+
with trace(workflow_name="test", metadata=metadata) as current_trace:
418+
with custom_span(name="direct_child", parent=current_trace) as direct_child:
419+
assert direct_child.trace_metadata == metadata
420+
with custom_span(name="parent") as parent:
421+
assert parent.trace_metadata == metadata
422+
with custom_span(name="child", parent=parent) as child:
423+
assert child.trace_metadata == metadata
424+
425+
426+
def test_processor_can_lookup_trace_metadata_by_span_trace_id():
427+
class MetadataPropagatingProcessor(TracingProcessor):
428+
def __init__(self) -> None:
429+
self.trace_metadata_by_id: dict[str, dict[str, Any]] = {}
430+
self.looked_up_metadata: dict[str, Any] | None = None
431+
self.span_trace_metadata: dict[str, Any] | None = None
432+
433+
def on_trace_start(self, trace: Trace) -> None:
434+
trace_metadata = getattr(trace, "metadata", None)
435+
if trace_metadata:
436+
self.trace_metadata_by_id[trace.trace_id] = dict(trace_metadata)
437+
438+
def on_trace_end(self, trace: Trace) -> None:
439+
return None
440+
441+
def on_span_start(self, span: Span[Any]) -> None:
442+
return None
443+
444+
def on_span_end(self, span: Span[Any]) -> None:
445+
if span.span_data.type != "agent":
446+
return
447+
self.looked_up_metadata = self.trace_metadata_by_id.get(span.trace_id)
448+
self.span_trace_metadata = span.trace_metadata
449+
450+
def shutdown(self) -> None:
451+
return None
452+
453+
def force_flush(self) -> None:
454+
return None
455+
456+
metadata = {
457+
"user_id": "u_123",
458+
"chat_type": "support",
459+
}
460+
processor = MetadataPropagatingProcessor()
461+
set_trace_processors([processor])
462+
try:
463+
with trace(workflow_name="workflow", metadata=metadata):
464+
with agent_span(name="agent"):
465+
pass
466+
finally:
467+
set_trace_processors([SPAN_PROCESSOR_TESTING])
468+
469+
assert processor.looked_up_metadata == metadata
470+
assert processor.span_trace_metadata == metadata
471+
472+
413473
def test_trace_to_json_only_includes_tracing_api_key_when_requested():
414474
with trace(workflow_name="test", tracing={"api_key": "secret-key"}) as tr:
415475
default_json = tr.to_json()

0 commit comments

Comments
 (0)