What is Baggage in Micrometer Tracing, and how do you propagate a business field (e.g. tenantId) and get it into logs?
answer
- Baggage = business key/values riding with the trace
- remote-fields = crosses the wire; correlation.fields = into MDC; local-fields = in-process only
- createBaggageInScope in try-with-resources; getBaggage().get()
- must declare field in config or it silently no-ops
- keep it small — every hop pays the header cost
basics
~20 sBaggage is arbitrary key/value data attached to the trace context and carried to downstream services alongside the trace IDs. Declare fields with management.tracing.baggage.remote-fields to propagate them, and correlation.fields to copy them into the logging MDC.
solid answer
~40 sTrace/span IDs identify *where* work happened; **Baggage** lets you attach *business* context — tenantId, userId, requestId — that rides with the trace across service boundaries. In Micrometer Tracing you set the value in a scope and Spring propagates it as extra headers to downstream calls. Configuration is key: `management.tracing.baggage.remote-fields=tenantId` makes it cross the wire; `management.tracing.baggage.correlation.fields=tenantId` copies it into the SLF4J **MDC** so `%X{tenantId}` appears in every log line on every service. `local-fields` stays in-process only. You write baggage with `Tracer.createBaggageInScope(name, value)` inside a try-with-resources block and read it with `tracer.getBaggage(name).get()`. Because baggage travels on every downstream request, keep fields few and small — it's not a general-purpose data channel.
code
java · 26 lines// application.properties
// management.tracing.baggage.remote-fields=tenantId
// management.tracing.baggage.correlation.fields=tenantId
// logging.pattern.level=%5p [%X{traceId:-},%X{spanId:-},tenant=%X{tenantId:-}]
@Service
class OrderFacade {
private final Tracer tracer;
private final OrderService orderService;
OrderFacade(Tracer tracer, OrderService orderService) {
this.tracer = tracer;
this.orderService = orderService;
}
void handle(String tenantId, Order order) {
// Opens a scope; tenantId is propagated downstream + copied into the MDC.
try (BaggageInScope bag = tracer.createBaggageInScope("tenantId", tenantId)) {
orderService.place(order); // any downstream HTTP/messaging call carries tenantId
} // scope closed -> baggage removed from context
}
String currentTenant() {
return tracer.getBaggage("tenantId").get();
}
}go deeper
Know baggage carries extra key/values with the trace and that config declares which fields propagate.
Distinguish remote-fields vs correlation.fields vs local-fields and wire a field into the MDC log pattern.
Manage scopes correctly, handle async context loss, and reason about header-size and security limits.
Set org-wide baggage governance: allowed fields, PII/secret exclusion, stripping at trust boundaries, and consistent naming across services.
**Baggage vs trace context.** The core trace context is just IDs (trace, span, flags). **Baggage** is a set of user-defined **key/value pairs** that ride *alongside* those IDs through the whole call graph. It answers 'which tenant / user / order is this trace about?' so you can correlate logs and spans by business identity, not just by IDs. **How it's carried.** Each remote baggage field becomes its own propagation header on outbound requests (e.g. a `tenantId` header, or under OTel a `baggage: tenantId=acme` header). Downstream services extract it and re-propagate it, so it flows the *entire* depth of the trace, not just one hop. **Three field categories in Spring Boot config** (`management.tracing.baggage.*`): - `remote-fields` — propagated over the wire to downstream services (added as headers). - `correlation.fields` — copied into the logging **MDC** so they show up in log patterns via `%X{field}`; requires `correlation.enabled=true` (default true). - `local-fields` — kept in the local context only; never propagated remotely. A field name can appear in both `remote-fields` and `correlation.fields` — commonly you want both (propagate *and* log). **Writing baggage — scope matters.** Baggage lives within a **scope**; leaving the scope removes it. Use try-with-resources so it's cleaned up: ```java try (BaggageInScope bag = tracer.createBaggageInScope("tenantId", tenant)) { orderService.place(order); // downstream calls carry tenantId } ``` Reading anywhere in scope: `tracer.getBaggage("tenantId").get()`. If a field is declared in config and arrives on an inbound request, Spring puts it in scope automatically for the request — you only call `createBaggageInScope` when *you* originate or override a value. **MDC / logging.** With `correlation.fields=tenantId` and a log pattern including `%X{tenantId}`, every log line across every service on the trace prints the tenant. This is the payoff: grep logs by business key, not just traceId. **Gotchas & limits.** - **Declare before use.** A field not listed in `remote-fields` won't cross the wire even if you set it in scope; not in `correlation.fields` won't reach the MDC. Silent no-ops otherwise. - **Overhead.** Every field is extra bytes on *every* downstream request header. Keep the set tiny and values short — some transports cap header size, and baggage bloats messaging payloads. - **Security.** Baggage is often forwarded blindly. Never put secrets/PII you wouldn't want logged or leaked to third-party services, and consider stripping baggage at trust boundaries (public egress). - **Scope leaks / async.** Losing the request thread (raw threads, un-instrumented executors) drops baggage just like trace context; use context-propagating executors or `ContextSnapshot`. - **Case / naming.** Field names are matched as configured; keep them consistent across services or propagation silently fails. **When to use.** Great for a handful of high-value correlation keys (tenantId, userId, requestId, feature-flag cohort). Not a transport for request payloads or large/volatile data.
- You set baggage in scope but downstream services don't receive it. What's the most likely cause?The field isn't declared in `management.tracing.baggage.remote-fields`. Baggage only propagates for configured remote fields; an undeclared field lives locally in the current context and never becomes an outbound header, so downstream extraction finds nothing.
- Baggage shows up downstream but not in that service's logs. Why?It's propagated (remote-fields) but not correlated — add the field to `management.tracing.baggage.correlation.fields` and include `%X{field}` in the log pattern. correlation.fields is what copies baggage into the SLF4J MDC.
- Why is putting a JWT or full user object into baggage a bad idea?Baggage is forwarded to every downstream service and typically copied into logs (MDC), so secrets/PII leak widely and it inflates every request header, risking header-size limits. Baggage is for small, non-sensitive correlation keys only.
saying these in an interview costs you the question
- Thinking baggage is automatically propagated without declaring remote-fields
- Assuming baggage appears in logs without correlation.fields + %X{} pattern
- Treating baggage as a general data channel for large/sensitive payloads
- Not closing the BaggageInScope (leaking scope) — should use try-with-resources
- Confusing baggage (business k/v) with the traceparent IDs