怎么让每个请求的日志都带同一个 trace id?为什么 @Async 和 CompletableFuture 里的日志没有 id?Callable 的日志呢?忘了清 MDC 会怎样?CommonsRequestLoggingFilter 打出来的日志安全吗?这篇在 Spring Boot 4.0.8 上逐个把线程名和 MDC 打印出来。
GET /work [X-Request-Id: req-A] → 200,响应头 X-Request-Id = req-A
[http-nio-9601-exec-2] traceId=[req-A] Main: controller 开始
[http-nio-9601-exec-2] traceId=[req-A] Svc: service 处理中
[http-nio-9601-exec-2] traceId=[req-A] Main: controller 结束
GET /work → 200,响应头 X-Request-Id = gen-1
[http-nio-9601-exec-3] traceId=[gen-1] Main: controller 开始
[http-nio-9601-exec-3] traceId=[gen-1] Svc: service 处理中
[http-nio-9601-exec-3] traceId=[gen-1] Main: controller 结束
GET /work [X-Request-Id: req-B] → 200,响应头 X-Request-Id = req-B
[http-nio-9601-exec-5] traceId=[req-B] Main: controller 开始
[http-nio-9601-exec-5] traceId=[req-B] Svc: service 处理中
[http-nio-9601-exec-5] traceId=[req-B] Main: controller 结束最靠前的 Filter 设置 MDC,日志格式里用 %X{traceId},业务代码不用传参数。
GET /async-plain [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9602-exec-2] traceId=[req-T] Main: controller 调用 @Async
[task-1] traceId=[] Svc: @Async 方法里
GET /cf [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9602-exec-3] traceId=[req-T] Main: controller 调用 CompletableFuture.supplyAsync
[ForkJoinPool.commonPool-worker-1] traceId=[] Main: supplyAsync 里(公共 ForkJoinPool)
GET /new-thread [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9602-exec-5] traceId=[req-T] Main: controller 开新线程
[raw-thread] traceId=[] Main: new Thread 里
GET /callable [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9602-exec-7] traceId=[req-T] Ic: 拦截器 preHandle [REQUEST]
[http-nio-9602-exec-7] traceId=[req-T] Main: controller 返回 Callable
[task-2] traceId=[] Main: Callable 里
[http-nio-9602-exec-8] traceId=[] Ic: 拦截器 preHandle [ASYNC]
[http-nio-9602-exec-8] traceId=[] Ic: 拦截器 afterCompletion [ASYNC]MDC 是线程局部的,@Async、Callable、CompletableFuture、new Thread 里都是空。
GET /async-plain [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9603-exec-2] traceId=[req-T] Main: controller 调用 @Async
[task-1] traceId=[req-T] Svc: @Async 方法里
GET /cf [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9603-exec-3] traceId=[req-T] Main: controller 调用 CompletableFuture.supplyAsync
[ForkJoinPool.commonPool-worker-1] traceId=[] Main: supplyAsync 里(公共 ForkJoinPool)
GET /new-thread [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9603-exec-5] traceId=[req-T] Main: controller 开新线程
[raw-thread] traceId=[] Main: new Thread 里
GET /callable [X-Request-Id: req-T] → 200,响应头 X-Request-Id = req-T
[http-nio-9603-exec-7] traceId=[req-T] Ic: 拦截器 preHandle [REQUEST]
[http-nio-9603-exec-7] traceId=[req-T] Main: controller 返回 Callable
[task-2] traceId=[req-T] Main: Callable 里
[http-nio-9603-exec-8] traceId=[] Ic: 拦截器 preHandle [ASYNC]
[http-nio-9603-exec-8] traceId=[] Ic: 拦截器 afterCompletion [ASYNC]只对 Spring 自己的执行器生效,别的线程要自己传递。
GET /plain [X-Request-Id: req-LEAK] → 200,响应头 X-Request-Id = req-LEAK
[http-nio-9604-exec-1] traceId=[req-LEAK] Main: controller 开始(没有带 X-Request-Id)
GET /plain → 200,响应头 X-Request-Id = None
[http-nio-9604-exec-1] traceId=[req-LEAK] Main: controller 开始(没有带 X-Request-Id)
GET /plain → 200,响应头 X-Request-Id = None
[http-nio-9604-exec-1] traceId=[req-LEAK] Main: controller 开始(没有带 X-Request-Id)不清 MDC,线程复用会让下一个请求继承上一个的 id。
POST /post?x=1&y=2 [X-Request-Id: req-R, Authorization: Bearer SECRET-TOKEN-123] → 200,响应头 X-Request-Id = req-R
[http-nio-9605-exec-2] traceId=[req-R] CommonsRequestLoggingFilter: Before request [POST /post?x=1&y=2]
[http-nio-9605-exec-2] traceId=[req-R] Main: controller 收到 body 长度 84
[http-nio-9605-exec-2] traceId=[req-R] CommonsRequestLoggingFilter: After request [POST /post?x=1&y=2, payload={"name":"Tom","note":"zzzzzzzz]payload 只在 After request 出现并被截断。
POST /post?x=1 [X-Request-Id: req-R, Authorization: Bearer SECRET-TOKEN-123, Cookie: sid=abc] → 200,响应头 X-Request-Id = req-R
[http-nio-9606-exec-2] traceId=[req-R] CommonsRequestLoggingFilter: Before request [POST /post?x=1, client=127.0.0.1, headers=[Host:"127.0.0.1:9606", Accept-Encoding:"identity", Content-Length:"7", X-Request-Id:"req-R", Authorization:"Bearer SECRET-TOKEN-123", Cookie:"sid=abc", Content-Type:"application/json;charset=UTF-8"]]
[http-nio-9606-exec-2] traceId=[req-R] Main: controller 收到 body 长度 7
[http-nio-9606-exec-2] traceId=[req-R] CommonsRequestLoggingFilter: After request [POST /post?x=1, client=127.0.0.1, headers=[Host:"127.0.0.1:9606", Accept-Encoding:"identity", Content-Length:"7", X-Request-Id:"req-R", Authorization:"Bearer SECRET-TOKEN-123", Cookie:"sid=abc", Content-Type:"application/json;charset=UTF-8"], payload={"a":1}]includeHeaders 会把凭据原样写进日志。
POST /post?x=1 [X-Request-Id: req-R, Authorization: Bearer SECRET-TOKEN-123, Cookie: sid=abc] → 200,响应头 X-Request-Id = req-R
[http-nio-9607-exec-2] traceId=[req-R] CommonsRequestLoggingFilter: Before request [POST /post?x=1, headers=[Host:"127.0.0.1:9607", Accept-Encoding:"identity", Content-Length:"7", X-Request-Id:"req-R", Cookie:"sid=abc", Content-Type:"application/json;charset=UTF-8", Authorization:"masked"]]
[http-nio-9607-exec-2] traceId=[req-R] Main: controller 收到 body 长度 7
[http-nio-9607-exec-2] traceId=[req-R] CommonsRequestLoggingFilter: After request [POST /post?x=1, headers=[Host:"127.0.0.1:9607", Accept-Encoding:"identity", Content-Length:"7", X-Request-Id:"req-R", Cookie:"sid=abc", Content-Type:"application/json;charset=UTF-8", Authorization:"masked"], payload={"a":1}]headerPredicate 只遮蔽指定的头,别的敏感头要自己考虑。