Skip to content

Commit 3afb5aa

Browse files
committed
feat(sdk): add W3C agent trace propagation helpers
1 parent 62e24c2 commit 3afb5aa

5 files changed

Lines changed: 82 additions & 2 deletions

File tree

packages/traceloop-sdk/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
Traceloop’s Python SDK allows you to easily start monitoring and debugging your LLM execution. Tracing is done in a non-intrusive way, built on top of OpenTelemetry. You can choose to export the traces to Traceloop, or to your existing observability stack.
44

5+
For agent-to-agent HTTP calls, use the opt-in W3C helpers to carry the current
6+
trace across service boundaries:
7+
8+
```python
9+
from traceloop.sdk import inject_trace_context, extract_trace_context
10+
11+
headers = inject_trace_context()
12+
remote_context = extract_trace_context(request.headers)
13+
```
14+
15+
`Traceloop.init()` configures W3C Trace Context and Baggage propagation by
16+
default. Existing custom propagators passed through `propagator=` remain
17+
supported.
18+
519
```python
620
Traceloop.init(app_name="joke_generation_service")
721

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from opentelemetry import context, trace
2+
from opentelemetry.sdk.trace import TracerProvider
3+
4+
from traceloop.sdk.propagation import extract_trace_context, inject_trace_context
5+
6+
7+
def test_inject_and_extract_trace_context_round_trip():
8+
provider = TracerProvider()
9+
tracer = provider.get_tracer("test")
10+
carrier = {}
11+
12+
with tracer.start_as_current_span("parent") as span:
13+
inject_trace_context(carrier)
14+
extracted = extract_trace_context(carrier)
15+
extracted_span = trace.get_current_span(extracted)
16+
17+
assert carrier["traceparent"].startswith("00-")
18+
assert f"-{span.get_span_context().span_id:016x}-" in carrier["traceparent"]
19+
assert extracted_span.get_span_context().trace_id == span.get_span_context().trace_id
20+
21+
22+
def test_inject_accepts_an_existing_carrier():
23+
carrier = {"x-request-id": "request-1"}
24+
result = inject_trace_context(carrier)
25+
26+
assert result is carrier
27+
assert result["x-request-id"] == "request-1"
28+
29+
30+
def test_extract_does_not_mutate_current_context():
31+
carrier = {}
32+
current = context.get_current()
33+
34+
extracted = extract_trace_context(carrier)
35+
36+
assert extracted is not current

packages/traceloop-sdk/traceloop/sdk/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
from typing import Dict
3434
from traceloop.sdk.client.client import Client
3535
from traceloop.sdk.associations.associations import AssociationProperty as AssociationProperty
36+
from traceloop.sdk.propagation import (
37+
extract_trace_context as extract_trace_context,
38+
inject_trace_context as inject_trace_context,
39+
)
3640

3741

3842
class Traceloop:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Opt-in W3C trace-context helpers for agent-to-agent calls."""
2+
3+
from typing import Any, MutableMapping
4+
5+
from opentelemetry import propagate
6+
from opentelemetry.context import Context
7+
8+
9+
def inject_trace_context(carrier: MutableMapping[str, Any] | None = None) -> MutableMapping[str, Any]:
10+
"""Inject the current W3C trace context into an HTTP-like carrier."""
11+
target: MutableMapping[str, Any] = carrier if carrier is not None else {}
12+
propagate.inject(target)
13+
return target
14+
15+
16+
def extract_trace_context(carrier: Any) -> Context:
17+
"""Extract a remote W3C trace context without changing the current context."""
18+
return propagate.extract(carrier)
19+

packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from opentelemetry.sdk.trace.sampling import Sampler
1818
from opentelemetry.propagators.textmap import TextMapPropagator
1919
from opentelemetry.propagate import set_global_textmap
20+
from opentelemetry.propagators.composite import CompositePropagator
21+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
22+
from opentelemetry.baggage.propagation import W3CBaggagePropagator
2023
from opentelemetry.sdk.trace.export import (
2124
SpanExporter,
2225
SimpleSpanProcessor,
@@ -162,8 +165,12 @@ def wrapped_on_end(span):
162165
obj.__spans_processor.on_start = obj._span_processor_on_start
163166
obj.__tracer_provider.add_span_processor(obj.__spans_processor)
164167

165-
if propagator:
166-
set_global_textmap(propagator)
168+
set_global_textmap(
169+
propagator
170+
or CompositePropagator(
171+
[TraceContextTextMapPropagator(), W3CBaggagePropagator()]
172+
)
173+
)
167174

168175
# this makes sure otel context is propagated so we always want it
169176
ThreadingInstrumentor().instrument()

0 commit comments

Comments
 (0)