Logs from ten services with no shared identifier are ten unrelated stories about one request. A trace ID is useful exactly as far as it survives every hop: a single service that loses the context breaks the whole chain — so propagation is an obligation at every boundary, not a library setting in one place.
The design comes from Google’s Dapper paper (Sigelman et al., “Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”, 2010): a trace id shared by every span of one request, a span id per unit of work, a parent id that turns the spans into a tree, all carried in RPC metadata. The wire format is now a standard, the W3C Trace Context recommendation: the traceparent header carries version, trace id, parent span id and a sampled flag, and tracestate carries vendor-specific data. OpenTelemetry implements both, which is what makes propagation an interoperable contract rather than a convention of one vendor’s agent.
// context flows through HTTP → Kafka → Flink automatically,
// if propagation is configured; by hand — inject/extract at every boundary
var headers = new RecordHeaders();
GlobalOpenTelemetry.getPropagators().getTextMapPropagator()
.inject(Context.current(), headers, (h, k, v) -> h.add(k, v.getBytes()));
producer.send(new ProducerRecord<>(topic, null, key, value, headers));// span hierarchy in code
#[tracing::instrument(skip(payload), fields(order_id = %payload.id))]
async fn process(payload: Order) -> Result<()> { ... }The Rust example shows only a local span. Context does not cross a network boundary on its own: you need an explicit inject on the producer and an extract on the consumer via TextMapPropagator. For Kafka — headers with a fixed encoding, without duplicating trace headers that already exist.
Where the chain usually breaks. Not at HTTP — middleware handles that — but at the asynchronous seams: a message published without headers, a batch job that consumes 500 events and emits one result, a retry scheduled on a fresh goroutine or executor without the captured context. For fan-in, a single parent is the wrong model; OpenTelemetry’s span links let one batch span point to every trace that contributed to it.
Limits. A trace id is a correlation key, not an identity or a business key: never authorize on it and never deduplicate by it. A traceparent arriving from the public internet is untrusted input (32) — a client can force sampling or collide ids — so the edge usually starts a new trace and keeps the incoming one as a link. And it does not belong in metric labels: unbounded cardinality breaks the metrics store; exemplars attach a few trace ids to a histogram instead.