From 8b74930417565ed1ef9a0fe58f8c3b97805070f6 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 13 Feb 2026 12:35:47 -0800 Subject: [PATCH] fix(tracing): propagate trace metadata to spans for processors --- src/agents/tracing/provider.py | 7 ++++ src/agents/tracing/spans.py | 12 +++++++ tests/test_tracing.py | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/agents/tracing/provider.py b/src/agents/tracing/provider.py index 0439b53d73..90ea85cbf0 100644 --- a/src/agents/tracing/provider.py +++ b/src/agents/tracing/provider.py @@ -309,6 +309,7 @@ def create_span( """ self._refresh_disabled_flag() tracing_api_key: str | None = None + trace_metadata: dict[str, Any] | None = None if self._disabled or disabled: logger.debug(f"Tracing is disabled. Not creating span {span_data}") return NoOpSpan(span_data) @@ -331,6 +332,8 @@ def create_span( parent_id = current_span.span_id if current_span else None trace_id = current_trace.trace_id tracing_api_key = current_trace.tracing_api_key + # Trace is an interface; custom implementations may omit metadata. + trace_metadata = getattr(current_trace, "metadata", None) elif isinstance(parent, Trace): if isinstance(parent, NoOpTrace): @@ -339,6 +342,8 @@ def create_span( trace_id = parent.trace_id parent_id = None tracing_api_key = parent.tracing_api_key + # Trace is an interface; custom implementations may omit metadata. + trace_metadata = getattr(parent, "metadata", None) elif isinstance(parent, Span): if isinstance(parent, NoOpSpan): logger.debug(f"Parent {parent} is no-op, returning NoOpSpan") @@ -346,6 +351,7 @@ def create_span( parent_id = parent.span_id trace_id = parent.trace_id tracing_api_key = parent.tracing_api_key + trace_metadata = parent.trace_metadata logger.debug(f"Creating span {span_data} with id {span_id}") @@ -356,6 +362,7 @@ def create_span( processor=self._multi_processor, span_data=span_data, tracing_api_key=tracing_api_key, + trace_metadata=trace_metadata, ) def shutdown(self) -> None: diff --git a/src/agents/tracing/spans.py b/src/agents/tracing/spans.py index 3ede9ebec6..e70c8780c5 100644 --- a/src/agents/tracing/spans.py +++ b/src/agents/tracing/spans.py @@ -178,6 +178,11 @@ def tracing_api_key(self) -> str | None: """The API key to use when exporting this span.""" pass + @property + def trace_metadata(self) -> dict[str, Any] | None: + """Trace-level metadata inherited by this span, if available.""" + return None + class NoOpSpan(Span[TSpanData]): """A no-op implementation of Span that doesn't record any data. @@ -266,6 +271,7 @@ class SpanImpl(Span[TSpanData]): "_processor", "_span_data", "_tracing_api_key", + "_trace_metadata", ) def __init__( @@ -276,6 +282,7 @@ def __init__( processor: TracingProcessor, span_data: TSpanData, tracing_api_key: str | None, + trace_metadata: dict[str, Any] | None = None, ): self._trace_id = trace_id self._span_id = span_id or util.gen_span_id() @@ -287,6 +294,7 @@ def __init__( self._prev_span_token: contextvars.Token[Span[TSpanData] | None] | None = None self._span_data = span_data self._tracing_api_key = tracing_api_key + self._trace_metadata = trace_metadata @property def trace_id(self) -> str: @@ -356,6 +364,10 @@ def ended_at(self) -> str | None: def tracing_api_key(self) -> str | None: return self._tracing_api_key + @property + def trace_metadata(self) -> dict[str, Any] | None: + return self._trace_metadata + def export(self) -> dict[str, Any] | None: return { "object": "trace.span", diff --git a/tests/test_tracing.py b/tests/test_tracing.py index df467f9052..ccbe2cfc7a 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -9,11 +9,13 @@ from agents.tracing import ( Span, Trace, + TracingProcessor, agent_span, custom_span, function_span, generation_span, handoff_span, + set_trace_processors, trace, ) from agents.tracing.spans import SpanError @@ -410,6 +412,64 @@ def test_trace_and_spans_use_tracing_config_key(): assert span.tracing_api_key == "tracing-key" +def test_trace_metadata_propagates_to_spans(): + metadata = {"source": "run"} + with trace(workflow_name="test", metadata=metadata) as current_trace: + with custom_span(name="direct_child", parent=current_trace) as direct_child: + assert direct_child.trace_metadata == metadata + with custom_span(name="parent") as parent: + assert parent.trace_metadata == metadata + with custom_span(name="child", parent=parent) as child: + assert child.trace_metadata == metadata + + +def test_processor_can_lookup_trace_metadata_by_span_trace_id(): + class MetadataPropagatingProcessor(TracingProcessor): + def __init__(self) -> None: + self.trace_metadata_by_id: dict[str, dict[str, Any]] = {} + self.looked_up_metadata: dict[str, Any] | None = None + self.span_trace_metadata: dict[str, Any] | None = None + + def on_trace_start(self, trace: Trace) -> None: + trace_metadata = getattr(trace, "metadata", None) + if trace_metadata: + self.trace_metadata_by_id[trace.trace_id] = dict(trace_metadata) + + def on_trace_end(self, trace: Trace) -> None: + return None + + def on_span_start(self, span: Span[Any]) -> None: + return None + + def on_span_end(self, span: Span[Any]) -> None: + if span.span_data.type != "agent": + return + self.looked_up_metadata = self.trace_metadata_by_id.get(span.trace_id) + self.span_trace_metadata = span.trace_metadata + + def shutdown(self) -> None: + return None + + def force_flush(self) -> None: + return None + + metadata = { + "user_id": "u_123", + "chat_type": "support", + } + processor = MetadataPropagatingProcessor() + set_trace_processors([processor]) + try: + with trace(workflow_name="workflow", metadata=metadata): + with agent_span(name="agent"): + pass + finally: + set_trace_processors([SPAN_PROCESSOR_TESTING]) + + assert processor.looked_up_metadata == metadata + assert processor.span_trace_metadata == metadata + + def test_trace_to_json_only_includes_tracing_api_key_when_requested(): with trace(workflow_name="test", tracing={"api_key": "secret-key"}) as tr: default_json = tr.to_json()