rgoussu@goussu: ~/library/platform/observability-and-sre
~/library/platform/observability-and-sre cat correlation-and-context-propagation.md

Correlation IDs & context propagation

# Extract-or-generate an id at the edge, carry it in a request context, stamp it on logs, responses, and outgoing calls — and where business correlation ids end and W3C traceparent/baggage begin.

Conceptsaved 2026-08-11 #observability#correlation-id#context-propagation#trace-context#distributed-tracing#middleware

Overview

No single log line tells the story of a request that crossed three services and a queue — correlation is what makes the pile of per-process logs navigable. The mechanic is small: at the edge of every service, one filter/middleware extracts or generates a request id, stores it in a request-scoped context, and from there it is stamped on every log line, echoed on the response, and forwarded on every outgoing call. The design questions worth getting right are the carrier (per platform), the header contract, and the boundary between hand-rolled business correlation ids and the W3C Trace Context / baggage standards that OpenTelemetry propagates automatically.

Key points

  • The edge algorithm: accept an inbound X-Correlation-Id header if present (trust boundary permitting), otherwise generate one (UUIDv4 is fine); store it in the request context; stamp it into the logging context (MDC); set it on the response so clients can quote it in bug reports; inject it into headers of every outgoing HTTP call and message published. One middleware does all of this — the id logic is never scattered through handlers.
  • Echo on the response is the underrated half: a client-visible id turns "it failed around noon" into a single log query.
  • Business correlation id ≠ trace id. W3C Trace Context defines traceparent (version-traceid-parentspanid-flags: a 16-byte trace-id, the caller's 8-byte span-id, sampling flags) and tracestate (vendor key-values). OTel propagators inject/extract it automatically on instrumented clients/servers — you get cross-service causality without writing propagation code. A business correlation id is different: user-meaningful, quotable, survives systems that have no tracing, and stable across a whole business transaction even where the trace is sampled away or split. Mature setups carry both and log both.
  • baggage (its own W3C header, key=value pairs) is the standards-track home for arbitrary cross-service context — tenant-id, channel, experiment flags. OTel propagates it alongside traceparent; entries must be explicitly copied onto spans/logs if you want them recorded (and they travel to every downstream service — never put secrets or PII in baggage).
  • Design an extensible request-context type, not a bare string: correlationId at minimum, extended with e.g. tenantId for multi-tenant scenarios — each parsed from its custom header by the same single filter/middleware at the edge, never re-parsed downstream. The context object is application-layer vocabulary; handlers receive it, the domain at most sees values it needs as ordinary parameters.
  • Carriers per stack: JVM — ThreadLocal + MDC set by a filter (Jakarta/JAX-RS ContainerRequestFilter, Spring OncePerRequestFilter, Micronaut HttpServerFilter); Go — context.Context values threaded explicitly; Rust — a tower/axum middleware (Layer) inserting a request extension and opening a tracing span with the id as a field; Node — AsyncLocalStorage entered by the first middleware.
  • Propagation is edge-to-edge: extract at the server edge, inject at the client edge. Every outgoing channel counts — HTTP clients, gRPC metadata, message headers (Kafka headers, AMQP properties); a queue that drops the id breaks the chain silently.

Details

The header contract

Header Standard Content Who propagates
traceparent W3C Trace Context trace-id, parent-span-id, flags OTel propagators, automatically
tracestate W3C Trace Context vendor-scoped key-values OTel propagators
baggage W3C Baggage arbitrary key-values (tenant-id, …) OTel propagators; opt-in to record
X-Correlation-Id (or X-Request-Id) convention one opaque business id your middleware, explicitly

Older conventions (X-B3-TraceId from Zipkin) still appear; OTel's composite propagator can speak several at once during migrations (OTEL_PROPAGATORS=tracecontext,baggage,b3multi).

Per-stack wiring

Stack Edge filter Context carrier Outgoing injection
Quarkus / Jakarta @Provider ContainerRequestFilter + ContainerResponseFilter MDC / ThreadLocal (mind async hops) ClientRequestFilter on the REST client
Spring Boot OncePerRequestFilter (register early, e.g. highest precedence) MDC ClientHttpRequestInterceptor / WebClient filter
Micronaut @Filter HttpServerFilter MDC (ServerRequestContext for reactive) HttpClientFilter
Go net/http func(http.Handler) http.Handler wrapper context.WithValue with unexported key type http.RoundTripper decorator
Rust axum tower::Layer middleware request extension + tracing span field reqwest middleware / manual header
Node http first middleware, als.run(store, next) AsyncLocalStorage fetch/undici wrapper reading the store

Two invariants across all six: the filter is registered at the application layer's driving adapter, and the wiring choice is made once at the assembly point — the hexagonal placement from the parent note.

Trust and validation

Accepting inbound ids is a trust decision: at a public edge, validate shape (length, charset) or replace outright — an attacker-controlled id lands verbatim in your logs (log-injection) and dashboards. Internal edges behind a gateway usually accept freely; the gateway generates.

Related

Citations

[1] W3C Trace Context [2] W3C Baggage [3] OpenTelemetry — context propagation