Observability & Tracing · OpenTelemetry course
Lesson 02
traceparent & baggageLesson 1 left one thing unexplained: how does payment-service know it belongs to a trace that started in gateway? Nothing is shared between those two JVMs except an HTTP request. So the answer has to be in the request. This lesson opens that request and reads the two headers that make distributed tracing possible.
The one win
By the end you can read a raw traceparent header field by field, and you can say exactly what baggage is, what it is not, and why it never shows up in Jaeger unless you make it.
Predict first
order-service calls payment-service over HTTP. The trace ID lives in memory in the caller's JVM. The callee is a different process, possibly a different machine. How does the trace ID get there? Answer in one word before reading on.
One word: headers. There is no magic, no shared database, no coordination service. The tracing context is serialised into HTTP headers on the way out and parsed back out of them on the way in. OpenTelemetry calls that whole mechanism context propagation, and the component that does it is a propagator.[context propagation]
The propagator is the only reason a trace survives a process boundary. Default format across the ecosystem: W3C Trace Context. [src]
In Spring Boot 3.x you never call inject/extract yourself — the instrumentation on RestTemplate, WebClient and Spring MVC does it for you. That is precisely why a trace "just works" across services once the dependencies are right.
traceparent, dissectedHere is a real one. Four fields, separated by hyphens, all lowercase hex:[w3c]
Two details worth burning in:
parent-id is the caller's span ID, not the callee's. The receiving service mints a new span ID and records this value as its parent. That is exactly the parent=9c41de arrow you saw in the Lesson 1 span tree — now you know which byte on the wire carried it.trace-flags carries the sampling decision. 01 means the caller recorded this trace, so downstream services should too. The decision is made once, at the start, and everyone downstream honours it — which is why a head sampling probability set on one service quietly governs the whole request path.In an inbound traceparent, what is the parent-id field?
There is a sibling header, tracestate, which carries vendor-specific key/value pairs alongside traceparent (max 32 entries). You will rarely touch it; it exists so multiple tracing systems can coexist on one request without trampling each other.[w3c]
Predict first
Only your gateway saw the auth token, so only the gateway knows this request belongs to tenant acme on the gold tier. Four hops later, payment-service wants that tenant ID. Without baggage, what are your options? Think for ten seconds before reading on.
Realistically two, and both are bad: thread tenantId through every method signature and every HTTP client in four services, or simply don't have it. Baggage is the third option. It is a key/value store that propagates alongside the trace context — set it once at the edge, read it anywhere downstream.[baggage]
You will see baggage called “not a signal” all over the internet, and Lesson 1 leaned on that shorthand. It is worth being precise, because the shorthand is wrong on the letter and right on the spirit.
On the letter: OpenTelemetry's own Signals page says “OpenTelemetry currently supports:” and then lists four items — Traces, Metrics, Logs, Baggage. It lives at /docs/concepts/signals/baggage/. By the project's own taxonomy, baggage is in the list.[signals]
On the spirit: the baggage page itself opens by defining it as “Contextual information that is passed between signals” — a description no one would write about traces. And every property that makes traces, metrics and logs alike is missing from baggage:
So the accurate sentence is: baggage is listed as a signal, but it is the only one that carries no telemetry. Nothing you put in baggage ever reaches Jaeger or Dynatrace on its own — which is exactly the trap in the next section.
What does a backend like Jaeger receive from baggage?
This is the one that costs people an afternoon. You set tenantId in baggage at the gateway, open Jaeger expecting to filter traces by it, and it is nowhere to be found. The value propagated perfectly. It was simply never recorded onto a span.
OpenTelemetry states it plainly: baggage "is a separate key-value store and is unassociated with attributes on spans, metrics, or logs without explicitly adding them."[baggage]
Attributes attach to one span and travel to the backend. Baggage attaches to the request and travels to the next service. Opposite directions — that is the whole confusion in one line.
To make baggage visible in your backend you must explicitly read it and set it as a span attribute. Some languages ship a Baggage Span Processor to automate that; in Spring Boot, the tag-fields property below does it for you.
You set userId in baggage, but cannot filter by it in Jaeger. Why?
W3C standardises a lowercase baggage header, separate from the trace context spec:[w3c baggage]
Everything lives under management.tracing.baggage.*, auto-wired by Actuator, enabled by default:[boot props]
Three notes specific to your stack, because you are on the OTel bridge and not Brave:
local-fields does not work for you. The Boot property docs say it outright: "Local fields are not supported with OpenTelemetry." It is a Brave-only feature.correlation.fields is the log-correlation payoff Lesson 1 pointed at. It pushes baggage into the MDC, so tenantId appears in every log line of the request, in every service — not just in the trace.get() can come back null. Code defensively.[micrometer]It is not tidiness. Instrumentation propagates baggage automatically on every outbound call the rest of the request tree makes — including calls to third parties. OpenTelemetry warns that "sensitive baggage items can be shared with unintended resources, like third-party APIs," and that "there are no built-in integrity checks to ensure that baggage items are yours" — so anything upstream can inject keys you will then read as trusted.[baggage]
W3C's rule is the one to memorise: ensure "no proprietary or confidential information is stored in baggage, or … that baggage isn't present in requests that cross trust-boundaries."[w3c baggage]
The working rule
Fine: tenant ID, tier, feature-flag cohort, region, request origin. Never: auth tokens, emails, anything you would redact from a log. And remember every entry is bytes added to every downstream request — it is not free either.
Answer out loud first, then reveal
Do not scroll back. If you cannot answer one, that is the section to re-read.
userId in baggage at the gateway. Why can't you filter by it in Jaeger?Because baggage is not span attributes. It propagated across every hop exactly as intended, but nothing ever wrote it onto a span, and only spans reach the backend. Fix it by copying baggage into attributes — in Spring, add the key to management.tracing.baggage.tag-fields.
traceparent?A separate, lowercase baggage header. It is independent of traceparent, which carries only the trace id, the caller's span id and the sampling flag. Both travel on the same request and are injected by the same propagation machinery, but either can exist without the other.
local-fields useless in your project specifically?Because it is Brave-only. Spring's own property docs state "Local fields are not supported with OpenTelemetry," and your project uses micrometer-tracing-bridge-otel.
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01Version 00; trace id 4bf92f35… (32 hex digits, identical on every span in the trace); parent id 00f067aa… (16 hex digits — the caller's span id, which becomes this span's parent); flags 01 = sampled, so this trace is being recorded.
Both, depending on what you mean, so answer precisely. OTel's Signals page does list it alongside traces, metrics and logs. But it is the only one of the four that has no data model, is never serialised into OTLP, and never reaches a backend — the baggage page defines it as “contextual information that is passed between signals.” The other three are telemetry; baggage is context that rides the request and dies with it.
Ten minutes on OpenTelemetry → Concepts → Context propagation, then skim the header grammar in the W3C Trace Context spec. The spec is short and unusually readable — you only need §3.2.
traceparent?”, “How does the sampling flag interact with management.tracing.sampling.probability?”, or “Show me baggage being set and read in real Spring code.”
traceparent / tracestate formatbaggage header, grammar and limitsmanagement.tracing.baggage.* properties