Skip to content

Commit 6d20b72

Browse files
authored
feat: Propagate W3C trace context headers from clients (#2153)
# What does this PR do? This extracts the W3C trace context headers (traceparent and tracestate) from incoming requests, stuffs them as attributes on the spans we create, and uses them within the tracing provider implementation to actually wrap our spans in the proper context. What this means in practice is that when a client (such as an OpenAI client) is instrumented to create these traces, we'll continue that distributed trace within Llama Stack as opposed to creating our own root span that breaks the distributed trace between client and server. It's slightly awkward to do this in Llama Stack because our Tracing API knows nothing about opentelemetry, W3C trace headers, etc - that's only knowledge the specific provider implementation has. So, that's why the trace headers get extracted by in the server code but not actually used until the provider implementation to form the proper context. This also centralizes how we were adding the `__root__` and `__root_span__` attributes, as those two were being added in different parts of the code instead of from a single place. Closes #2097 ## Test Plan This was tested manually using the helpful scripts from #2097. I verified that Llama Stack properly joined the client's span when the client was instrumented for distributed tracing, and that Llama Stack properly started its own root span when the incoming request was not part of an existing trace. Here's an example of the joined spans: ![Screenshot 2025-05-13 at 8 46 09 AM](https://github.com/user-attachments/assets/dbefda28-9faa-4339-a08d-1441efefc149) Signed-off-by: Ben Browning <[email protected]>
1 parent 82778ec commit 6d20b72

File tree

3 files changed

+33
-4
lines changed

3 files changed

+33
-4
lines changed

llama_stack/distribution/server/server.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,18 @@ async def __call__(self, scope, receive, send):
280280
logger.debug(f"No matching endpoint found for path: {path}, falling back to FastAPI")
281281
return await self.app(scope, receive, send)
282282

283-
trace_context = await start_trace(trace_path, {"__location__": "server", "raw_path": path})
283+
trace_attributes = {"__location__": "server", "raw_path": path}
284+
285+
# Extract W3C trace context headers and store as trace attributes
286+
headers = dict(scope.get("headers", []))
287+
traceparent = headers.get(b"traceparent", b"").decode()
288+
if traceparent:
289+
trace_attributes["traceparent"] = traceparent
290+
tracestate = headers.get(b"tracestate", b"").decode()
291+
if tracestate:
292+
trace_attributes["tracestate"] = tracestate
293+
294+
trace_context = await start_trace(trace_path, trace_attributes)
284295

285296
async def send_with_trace_id(message):
286297
if message["type"] == "http.response.start":

llama_stack/providers/inline/telemetry/meta_reference/telemetry.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from opentelemetry.sdk.trace import TracerProvider
1717
from opentelemetry.sdk.trace.export import BatchSpanProcessor
1818
from opentelemetry.semconv.resource import ResourceAttributes
19+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
1920

2021
from llama_stack.apis.telemetry import (
2122
Event,
@@ -44,6 +45,7 @@
4445
)
4546
from llama_stack.providers.utils.telemetry.dataset_mixin import TelemetryDatasetMixin
4647
from llama_stack.providers.utils.telemetry.sqlite_trace_store import SQLiteTraceStore
48+
from llama_stack.providers.utils.telemetry.tracing import ROOT_SPAN_MARKERS
4749

4850
from .config import TelemetryConfig, TelemetrySink
4951

@@ -206,6 +208,15 @@ def _log_structured(self, event: StructuredLogEvent, ttl_seconds: int) -> None:
206208
event.attributes = {}
207209
event.attributes["__ttl__"] = ttl_seconds
208210

211+
# Extract these W3C trace context attributes so they are not written to
212+
# underlying storage, as we just need them to propagate the trace context.
213+
traceparent = event.attributes.pop("traceparent", None)
214+
tracestate = event.attributes.pop("tracestate", None)
215+
if traceparent:
216+
# If we have a traceparent header value, we're not the root span.
217+
for root_attribute in ROOT_SPAN_MARKERS:
218+
event.attributes.pop(root_attribute, None)
219+
209220
if isinstance(event.payload, SpanStartPayload):
210221
# Check if span already exists to prevent duplicates
211222
if span_id in _GLOBAL_STORAGE["active_spans"]:
@@ -216,8 +227,12 @@ def _log_structured(self, event: StructuredLogEvent, ttl_seconds: int) -> None:
216227
parent_span_id = int(event.payload.parent_span_id, 16)
217228
parent_span = _GLOBAL_STORAGE["active_spans"].get(parent_span_id)
218229
context = trace.set_span_in_context(parent_span)
219-
else:
220-
event.attributes["__root_span__"] = "true"
230+
elif traceparent:
231+
carrier = {
232+
"traceparent": traceparent,
233+
"tracestate": tracestate,
234+
}
235+
context = TraceContextTextMapPropagator().extract(carrier=carrier)
221236

222237
span = tracer.start_span(
223238
name=event.payload.name,

llama_stack/providers/utils/telemetry/tracing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
INVALID_SPAN_ID = 0x0000000000000000
3535
INVALID_TRACE_ID = 0x00000000000000000000000000000000
3636

37+
ROOT_SPAN_MARKERS = ["__root__", "__root_span__"]
38+
3739

3840
def trace_id_to_str(trace_id: int) -> str:
3941
"""Convenience trace ID formatting method
@@ -178,7 +180,8 @@ async def start_trace(name: str, attributes: dict[str, Any] = None) -> TraceCont
178180

179181
trace_id = generate_trace_id()
180182
context = TraceContext(BACKGROUND_LOGGER, trace_id)
181-
context.push_span(name, {"__root__": True, **(attributes or {})})
183+
attributes = {marker: True for marker in ROOT_SPAN_MARKERS} | (attributes or {})
184+
context.push_span(name, attributes)
182185

183186
CURRENT_TRACE_CONTEXT.set(context)
184187
return context

0 commit comments

Comments
 (0)