Spring Boot 3中Micrometer Tracing与MDC集成问题咨询
需求背景
基于Logback(Slf4j)实现自定义链路追踪与日志管理,需从请求头读取字段加入关联信息,同时向MDC添加自定义字段用于日志输出。从Spring Boot 2的Sleuth切换到Spring Boot 3的Micrometer后,遇到MDC数据管理、自定义字段添加、线程上下文传递的问题,具体疑问:
- 如何正确向关联信息添加自定义字段?
- 使用Micrometer时如何填充MDC?
- 如何将上下文从父线程传递到新线程,并在任务完成后清理?
已完成的配置
1. 日志格式配置
logging: pattern: console: "%clr(%d){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%X{testRemoteBaggage}-%X{testLocalBaggage}]){magenta} %clr([%40.40t]){faint} %clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"
2. 自定义关联字段配置
management: tracing: sampling: probability: 1.0 baggage: correlation: fields: testRemoteBaggage, testLocalBaggage remote-fields: testRemoteBaggage local-fields: testLocalBaggage
3. 虚拟线程异步任务执行器配置
application.yml
spring: threads: virtual: enabled: true
配置类
@EnableAsync @Configuration(proxyBeanMethods = false) public class DummyJsonTracingConfiguration { @Bean public AsyncTaskExecutor applicationTaskExecutor() { return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); } @Bean public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; } }
4. 自定义Filter添加关联字段
@Component @Slf4j @RequiredArgsConstructor public class AddBaggageFilter extends OncePerRequestFilter implements OrderedFilter { private static final String fieldRemote = "testRemoteBaggage"; private static final String fieldLocal = "testLocalBaggage"; private final Tracer tracer; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String baggagefieldRemote = request.getHeader(fieldRemote); if (StringUtils.isEmpty(baggagefieldRemote)) { baggagefieldRemote = "autoValue"+fieldRemote; } String baggagefieldLocal = request.getHeader(fieldLocal); if (StringUtils.isEmpty(baggagefieldLocal)) { baggagefieldLocal = "autoValue"+fieldLocal; } log.warn("baggagefieldRemote value : [{}]", baggagefieldRemote); log.warn("baggagefieldLocal value : [{}]", baggagefieldLocal); try (BaggageInScope baggageRemote = tracer.createBaggageInScope(fieldRemote, baggagefieldRemote); BaggageInScope baggageLocal = tracer.createBaggageInScope(fieldLocal, baggagefieldLocal)) { log.warn("baggagefieldRemote was created!"); filterChain.doFilter(request, response); } } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 2; } }
当前问题
日志中仅主线程显示关联信息,异步线程(如ForkJoinPool中的线程)的日志丢失了testRemoteBaggage和testLocalBaggage字段:
2024-07-14 14:05:28,274 WARN 23716 --- [test1-] [ tomcat-handler-1] [6693bf08c4ecb0e29507c9dbc43570fe-9507c9dbc43570fe] b.g.t.rest.AddBaggageFilter : baggagefieldRemote value : [test1] 2024-07-14 14:05:28,275 WARN 23716 --- [test1-] [ tomcat-handler-1] [6693bf08c4ecb0e29507c9dbc43570fe-9507c9dbc43570fe] b.g.t.rest.AddBaggageFilter : baggagefieldLocal value : [autoValuetestLocalBaggage] 2024-07-14 14:05:28,276 WARN 23716 --- [test1-autoValuetestLocalBaggage] [ tomcat-handler-1] [6693bf08c4ecb0e29507c9dbc43570fe-9507c9dbc43570fe] b.g.t.rest.AddBaggageFilter : baggagefieldRemote was created! 2024-07-14 14:05:28,279 DEBUG 23716 --- [test1-autoValuetestLocalBaggage] [ tomcat-handler-1] [6693bf08c4ecb0e29507c9dbc43570fe-9507c9dbc43570fe] o.s.web.servlet.DispatcherServlet : POST "/categoryInfo", parameters={} 2024-07-14 14:05:28,285 DEBUG 23716 --- [test1-autoValuetestLocalBaggage] [ tomcat-handler-1] [6693bf08c4ecb0e29507c9dbc43570fe-9507c9dbc43570fe] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to by.gvu.testtracingrestclient.controller.SimpleController#getListProducts() 2024-07-14 14:05:29,128 DEBUG 23716 --- [test1-autoValuetestLocalBaggage] [ tomcat-handler-1] [6693bf08c4ecb0e29507c9dbc43570fe-9507c9dbc43570fe] o.s.web.client.DefaultRestClient : Reading to [java.util.List<java.lang.String>] 2024-07-14 14:05:29,377 DEBUG 23716 --- [-] [ ForkJoinPool.commonPool-worker-9] [ ] o.s.web.client.DefaultRestClient : Reading to [java.util.List<by.gvu.testtracingrestclient.model.Product>] 2024-07-14 14:05:29,689 DEBUG 23716 --- [-] [ ForkJoinPool.commonPool-worker-10] [ ] o.s.web.client.DefaultRestClient : Reading to [java.util.List<by.gvu.testtracingrestclient.model.Product>] 2024-07-14 14:05:29,734 DEBUG 23716 --- [-] [ ForkJoinPool.commonPool-worker-4] [ ] o.s.web.client.DefaultRestClient : Reading to [java.util.List<by.gvu.testtracingrestclient.model.Product>] 2024-07-14 14:05:29,734 DEBUG 23716 --- [-] [ ForkJoinPool.commonPool-worker-12] [ ] o.s.web.client.DefaultRestClient : Reading to [java.util.List<by.gvu.testtracingrestclient.model.Product>]
更新:问题与虚拟线程无关。
1. 正确向关联信息添加自定义字段
你当前的配置已经正确定义了自定义baggage字段,核心规则如下:
remote-fields:配置的字段会在跨服务调用时通过请求头传递,需确保上下游服务配置相同字段local-fields:仅当前服务内部使用,不会传递到其他服务- 代码中通过
tracer.createBaggageInScope设置字段值的方式是正确的,Filter设置Ordered.HIGHEST_PRECEDENCE + 2的顺序也没问题,能保证在业务逻辑执行前完成baggage初始化。
额外优化:Micrometer支持自动从请求头提取同名baggage字段,若无需自定义默认值逻辑,可以去掉Filter中的头读取代码,依赖框架自动注入。
2. 使用Micrometer时如何填充MDC
Micrometer Tracing会自动将management.tracing.baggage.correlation.fields中的字段同步到MDC,无需手动添加。你的配置已经将testRemoteBaggage和testLocalBaggage加入该列表,只要baggage存在,MDC就会自动填充对应字段。
可通过业务代码中调用MDC.get("testRemoteBaggage")验证同步效果。
3. 线程上下文传递与清理
异步线程丢失上下文的核心原因是:默认线程池(包括ForkJoinPool)未配置上下文传递包装器,Micrometer上下文不会自动传递到非Spring管理的异步任务中。
解决步骤:
(1)为自定义线程池添加上下文包装
修改applicationTaskExecutor的配置,用ContextExecutorService包装线程池,确保上下文传递:
@Bean public AsyncTaskExecutor applicationTaskExecutor() { ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); // 包装线程池,传递Micrometer上下文 ExecutorService wrappedExecutor = ContextExecutorService.wrap(executor, ContextSnapshot::captureAll); return new TaskExecutorAdapter(wrappedExecutor); }
(2)处理ForkJoinPool的上下文传递
若代码中使用CompletableFuture等默认依赖ForkJoinPool的异步操作,需手动包装任务,或配置全局自定义ForkJoinPool:
- 提交任务时直接包装:
CompletableFuture.runAsync(ContextSnapshot.wrap(() -> { // 异步业务逻辑 }));
- 配置全局自定义ForkJoinPool:
@Bean public ForkJoinPool forkJoinPool() { return new ForkJoinPool( Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, false ); } // 使用时注入并包装任务 @Autowired private ForkJoinPool forkJoinPool; CompletableFuture.runAsync(ContextSnapshot.wrap(() -> { // 业务逻辑 }), forkJoinPool);
(3)上下文清理
你在Filter中使用的try-with-resources方式会自动清理BaggageInScope上下文,是正确的。对于异步任务,ContextSnapshot.wrap会在任务执行完成后自动恢复原上下文,无需手动清理。
额外注意点
- 确保引入正确依赖:
micrometer-tracing-bridge-brave(Spring Boot 3 starter已默认包含,需检查版本匹配) - 使用
@Async注解时,Spring会自动使用你配置的applicationTaskExecutor,只要线程池已被上下文包装,异步方法会自动继承父线程上下文。
内容的提问来源于stack exchange,提问作者Heorhi Utseuski

