skip to content

How would you add a custom field (e.g. userId or tenantId) to every log line AND propagate it downstream alongside traceId? Contrast MDC vs baggage.

level: principalimportance: nice to knowfreq 30%

answer

  1. MDC = local thread only; baggage = crosses services
  2. remote-fields (wire) + correlation.fields (MDC)
  3. tracer.createBaggageInScope, close it
  4. MDC.putCloseable to avoid pooled-thread leaks
  5. never put secrets/PII in baggage — travels in headers

basics

~20 s

Plain MDC.put adds a field to local logs only and doesn't cross services. Use Micrometer Tracing baggage: create a baggage field, and configure it to be written into MDC and propagated in headers. Then userId/tenantId appears in every service's logs, not just the first.

solid answer

~50 s

Two distinct tools. **MDC** is local: `MDC.put("tenantId", ...)` (ideally via try-with-resources `MDC.putCloseable`) makes the value printable with `%X{tenantId}` on that thread — but it stops at the process edge and is lost across threads. **Baggage** is Micrometer Tracing's mechanism for user-defined key/values that ride *with the trace context* across service boundaries in propagation headers. You declare fields via `management.tracing.baggage.remote-fields` (propagated over the wire) and `management.tracing.baggage.correlation.fields` (copied into MDC so they print in logs). Set a value with the `BaggageManager`/`Tracer` baggage API inside a scope; it then flows to downstream services and appears in *their* MDC/logs too. Use MDC for purely local context; use baggage when the field must travel with the request across services and threads. Beware baggage cost — every field is copied into every header and log, so keep it small and never put secrets there.

code

java · 21 lines
java
// application.yml
// management.tracing.baggage.remote-fields: tenantId
// management.tracing.baggage.correlation.fields: tenantId
// logging.pattern.correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-},%X{tenantId:-}] "

@Component
class TenantBaggageFilter extends OncePerRequestFilter {
  private final Tracer tracer;
  TenantBaggageFilter(Tracer tracer) { this.tracer = tracer; }

  @Override protected void doFilterInternal(HttpServletRequest req,
      HttpServletResponse res, FilterChain chain)
      throws ServletException, IOException {
    String tenant = resolveTenant(req); // e.g. from JWT claim
    try (BaggageInScope bag = tracer.createBaggageInScope("tenantId", tenant)) {
      // logs here + in downstream services print tenantId; header propagates it
      chain.doFilter(req, res);
    }
  }
  private String resolveTenant(HttpServletRequest r) { return r.getHeader("X-Tenant"); }
}

go deeper

for a junior

Might only know MDC.put adds a log field.

for a middle

Knows MDC is local and there's a way to propagate custom fields.

for a senior

Can configure baggage remote-fields + correlation.fields and set it at the edge.

for a principal

Weighs cost/security/cardinality of baggage, standardizes fields across the fleet, and enforces edge validation and no-secrets policy.

## MDC vs baggage — the core distinction ### MDC (Mapped Diagnostic Context) - A thread-local `Map<String,String>` in SLF4J/Logback. - You add fields yourself: `MDC.put("tenantId", t)` and print with `%X{tenantId}`. - **Scope**: local to the current thread and current process. It does **not** propagate to other threads (thread-local) or to other services (never serialized onto the wire by itself). - Correct usage is try-with-resources so you don't leak into pooled threads: ```java try (MDC.MDCCloseable c = MDC.putCloseable("tenantId", tenantId)) { log.info("processing"); // prints tenantId } // auto-removed ``` ### Baggage (Micrometer Tracing) - **Baggage** = arbitrary user-defined key/value pairs that are part of the *trace context* and are **propagated across service boundaries** in the same headers as the trace (W3C `baggage` header, or B3 with Brave). - Managed via the tracer's baggage API (`Tracer#createBaggageInScope` / `BaggageManager`), not `MDC.put`. - Configured declaratively in Spring Boot: - `management.tracing.baggage.remote-fields: tenantId,userId` — these keys are **propagated over the wire** to downstream services. - `management.tracing.baggage.correlation.fields: tenantId,userId` — these keys are **copied into MDC** so `%X{tenantId}` prints them in logs. - `management.tracing.baggage.enabled` (default true) toggles the feature. - Because it rides the trace context, baggage survives *both* thread hops (when context propagation is set up) *and* service hops (via headers). ## Recipe: userId/tenantId in every service's logs 1. Configure: ```yaml management: tracing: baggage: remote-fields: [tenantId, userId] correlation: fields: [tenantId, userId] ``` 2. Add the keys to your log pattern if you want them explicitly (or rely on structured logging emitting MDC): ```yaml logging: pattern: correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-},%X{tenantId:-}] " ``` 3. Set the value once, early (e.g., an interceptor after auth), inside a scope: ```java try (BaggageInScope bag = tracer.createBaggageInScope("tenantId", tenantId)) { chain.doFilter(req, res); // downstream + logs now carry tenantId } ``` From here on, the current service's logs show `tenantId`, the value is injected into outgoing propagation headers, and each downstream service extracts it into its own MDC/logs. ## Gotchas and trade-offs - **Cost**: baggage is copied into *every* propagation header on *every* outbound call and into MDC on every log line. Large or numerous baggage fields inflate every request and log — keep it to a few small identifiers. - **Security**: baggage crosses trust boundaries in plain headers. **Never** put secrets, tokens, or PII you wouldn't expose to downstream services / logs. It's also attacker-influenceable if an untrusted client sets the header — validate/allowlist at the edge. - **MDC leaks**: if you `MDC.put` without removing (no try-with-resources), pooled threads carry the stale value into the next unrelated request. Baggage's scope objects avoid this when closed properly. - **Case/format**: baggage keys are case-sensitive strings; keep naming consistent across services or correlation.fields won't match. - **Propagation must be enabled** for cross-thread flow (see async propagation): baggage in MDC on a worker thread still requires context propagation to be set up. - **Not for high-cardinality bulk data**: baggage is for identifiers to correlate, not for shipping payloads. ## When to use which - **MDC only** — a value relevant to this service's logs and never needed elsewhere (e.g., an internal batch id computed locally). - **Baggage** — a value that must appear in logs/traces of *downstream* services too (tenantId, userId, requestId from the edge). Configure `remote-fields` (wire) + `correlation.fields` (MDC) and set it once at the boundary.

  • You added tenantId via MDC.put in an auth filter and it prints locally, but downstream services don't show it. Why, and what's the fix?
    Plain MDC is process-local and never serialized onto propagation headers, so it can't reach downstream services. Use baggage instead: declare tenantId in management.tracing.baggage.remote-fields (to propagate over the wire) and correlation.fields (to print via MDC), and set it with the tracer's baggage API.
  • What are the risks of putting a lot in baggage?
    Every field is copied into every outbound propagation header and into every log line, inflating request size and log volume. It also crosses trust boundaries in plain headers, so secrets/PII must never go there, and untrusted clients can spoof the header — validate at the edge. Keep baggage to a few small identifiers.

saying these in an interview costs you the question

  • Believing MDC.put propagates to downstream services
  • Putting secrets, tokens, or PII into baggage (it travels in plaintext headers)
  • Forgetting correlation.fields, so baggage propagates but never appears in logs
  • Using MDC.put without removal, leaking values across pooled threads
  • Treating baggage as a general data-transport channel rather than small correlation identifiers

context