Spring Cloud Sleuth 必须通过 Brave 的 TraceContext 机制(如 Baggage)跨线程/跨服务透传 userId 等字段,MDC 因仅限当前线程而不可靠;Baggage 支持自动 HTTP 头传播(baggage-user-id)和 Zipkin 按值查询。

Spring Cloud Sleuth 默认只注入 traceId 和 spanId,业务系统常需额外透传如 userId、tenantId、requestSource 等上下文字段——这些不能靠日志 MDC 手动塞,否则跨线程、跨服务会丢失。必须走 Brave 的 TraceContext 传播机制,否则 Zipkin UI 里查不到、下游服务也拿不到。
为什么不能只用 MDC.put("userId", "123")?
MDC 是线程局部变量,仅在当前线程有效。一旦遇到异步(CompletableFuture、@Async)、线程池(ThreadPoolTaskExecutor)、消息中间件(Kafka、RabbitMQ)或 Feign 调用,MDC 内容就断了。Sleuth 的 TraceContext 则通过 Brave 的 CurrentTraceContext + Scope 机制自动跨线程/跨服务传递,这才是可靠路径。
如何用 Brave 的 Customizer 注入自定义字段?
关键不是改日志格式,而是把业务标识写进 Span 的 tags 或 Baggage。推荐优先用 Baggage:它支持自动 HTTP 头透传(baggage-xxx),且 Zipkin 支持按 baggage 查询,比 tags 更语义清晰。
- 在启动类或配置类中声明
BaggageField:@Bean public BaggageField userIdBaggageField() { return BaggageField.create("user-id"); } - 注册
CurrentTraceContextCustomizer,让 Baggage 随 TraceContext 自动传播:@Bean public CurrentTraceContextCustomizer currentTraceContextCustomizer( BaggageField userIdBaggageField) { return builder -> builder.addScopeDecorator( BaggagePropagationScopeDecorator.create(userIdBaggageField)); } - 在请求入口(如 Filter 或 WebMvcConfigurer)中设置值:
userIdBaggageField.updateValue(Tracing.current().tracer().currentSpan(), "U987654321");
Feign / RestTemplate 调用时如何确保 Baggage 透传?
Sleuth 默认已集成 Brave 的 HttpClientParser 和 OkHttpParser,但前提是你的 Feign 客户端用了 spring-cloud-starter-openfeign(而非裸用 OpenFeign),且未覆盖默认的 Feign.Builder。常见踩坑点:
- 手动 new
Feign.builder()会绕过 Sleuth 自动装饰,必须用FeignAutoConfiguration提供的 bean; - RestTemplate 需启用拦截器:
RestTemplate restTemplate = new RestTemplate(new TraceRestTemplateInterceptor(tracer, spanNamer));,但更推荐直接用 Sleuth 自动装配的RestTemplatebean(加@LoadBalanced即可); - HTTP header 名默认是
baggage-user-id,下游服务需同样配置BaggageField.create("user-id")才能读取,否则值为空。
自定义字段在 Zipkin 和日志中怎么查?
Baggage 字段不会自动出现在 Zipkin UI 的 Span 标签栏,需显式调用 Span.tag("user-id", value) 才可见;但查询时仍可用 user-id:U987654321 过滤整条 Trace。日志中若用 Logback + %X{user-id},需配合 Slf4jScopeDecorator 启用 MDC 自动同步(Sleuth 3.1+ 默认开启,老版本需手动配)。
最易忽略的是:Baggage 值变更后,必须调用 updateValue(),而不是 getValue() —— 后者只读,且返回的是上一个 Scope 的值,不是当前 Span 的。

















