第 46 章:日志与链路追踪
学习目标
- 用 Logback 输出结构化日志
- 用 MDC 实现请求链路追踪
- 学会日志级别与生产配置
一、Logback 基础
Spring Boot 默认用 Logback,无需配置。
@Slf4j // Lombok 自动生成 private static final Logger log
@Service
public class UserService {
public void create(User user) {
log.info("创建用户: {}", user); // 占位符,不拼字符串
log.debug("详细参数: {}", user.getId());
log.error("失败", e); // 异常:最后一个参数
}
}日志级别(从低到高):
| 级别 | 用途 |
|---|---|
TRACE | 最详细,基本不用 |
DEBUG | 开发 / 调试 |
INFO | 业务关键信息(默认) |
WARN | 警告,需要关注 |
ERROR | 错误,需要处理 |
二、Logback 配置
src/main/resources/logback-spring.xml:
<configuration>
<!-- ① 输出格式 -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"/>
<!-- ② 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- ③ 文件输出 + 滚动 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- ④ 级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- ⑤ 第三方包按需调 -->
<logger name="com.example" level="DEBUG"/>
<logger name="org.springframework.web" level="INFO"/>
</configuration>⚠️ 坑 1:文件名必须是
logback-spring.xml而不是logback.xml,前者支持 Spring profile,后者在 Spring 启动前加载。
三、链路追踪(MDC)
问题:一次请求经过多个方法,日志分散,怎么串起来?
解决:用 MDC(Mapped Diagnostic Context)放 traceId,过滤器在每个请求开头自动注入。
3.1 完整定义(过滤器)
// MDC 从哪来:import org.slf4j.MDC;
// SLF4J(日志门面)提供的静态工具类,不用注入、不用 new,直接用 MDC.put()
// 它内部是个"线程私有 map"(ThreadLocal),每个线程各自一份。
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component // ① 让 Spring 扫描到、创建它(关键)
@Order(Ordered.HIGHEST_PRECEDENCE) // ② 在所有过滤器里排最前,第一个执行
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) {
// 有网关/上游传来的 traceId 就用,没有就自己造一个
String traceId = req.getHeader("X-Trace-Id");
if (traceId == null) traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put("traceId", traceId); // ③ 写进【当前线程】的 MDC
resp.setHeader("X-Trace-Id", traceId); // ④ 放响应头,方便前端/下游对齐
try {
chain.doFilter(req, resp); // ⑤ 放行 → controller → service,同一线程
} finally {
MDC.remove("traceId"); // ⑥ 用完清掉,否则线程复用会污染
}
}
}3.2 MDC 从哪来?(上面代码里 MDC 没体现,特此说明)
MDC 是 SLF4J 库里的静态类,不是 Spring 的 bean,所以没有 @Autowired、不用 new,直接 MDC.put(...) 就能用。它由 spring-boot-starter-logging(Spring Boot 自带的日志依赖)传递引入,所以项目里天然就有。
- 本质:线程私有 map(ThreadLocal)。
MDC.put("traceId", x)只写进当前线程。 - 为什么整条链都有?过滤器在请求最开头执行,
chain.doFilter之后 controller/service 仍在同一线程跑,所以它们打日志时 MDC 里还带着 traceId。 - 日志怎么读到?logback 的 pattern 里
%X{traceId}= 去当前线程 MDC 取 key 为traceId的值。
3.3 Spring Boot 怎么扫描到并启用它?
分两步,都是 Spring Boot 自动完成的:
- 组件扫描:
@SpringBootApplication扫自己包及子包,@Component标注的TraceIdFilter被找到 → 成为容器里的 bean。 - 自动注册成过滤器:Spring Boot 有个机制——只要某个 bean 是
Filter的实现类(OncePerRequestFilter就是),它就自动帮你注册成 Servlet 过滤器,并且每个请求都会经过它,无需手动配置。
若想精确控制顺序 / 只拦某些路径,可改用显式注册
FilterRegistrationBean:java@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<TraceIdFilter> traceIdFilter() { FilterRegistrationBean<TraceIdFilter> reg = new FilterRegistrationBean<>(); reg.setFilter(new TraceIdFilter()); reg.addUrlPatterns("/*"); // 拦所有路径 reg.setOrder(1); // 控制执行顺序 return reg; } }
<!-- logback-spring.xml 的 pattern 里加上 -->
<pattern>%d{...} [%thread] [%X{traceId}] %-5level %logger - %msg%n</pattern>输出:
2026-08-14 10:30:45.123 [http-nio-8080-exec-1] [a1b2c3d4...] INFO UserService - 创建用户⚠️ 坑 2:
MDC一定要在finally里remove(),否则线程池复用时会带到下一个请求。
四、MDC + 异步
⚠️ 这节属于进阶场景,日常多数业务是同步的,用不上。了解"为什么丢、怎么复印回去"的原理即可,不用背代码。 实际生产若要跨线程/跨服务传 traceId,更常用现成的链路追踪框架(SkyWalking、Micrometer Tracing + Zipkin)。
为什么异步会丢? MDC 是线程私有的,主线程纸上的 traceId 只属于主线程;一换到新线程,新线程拿到的是空白纸,日志就没有 traceId 了。要解决,就得把主线程纸上的内容复印一份贴到新线程纸上。
办法一:手动拷(每个任务里写)
Runnable task = () -> {
MDC.put("traceId", traceId); // 拷到新线程的纸上
try {
doWork();
} finally {
MDC.remove("traceId"); // 用完清掉
}
};
new Thread(task).start();办法二:TaskDecorator(线程池自动复印,一处配置全生效)
思路:给线程池设一个"包装器",每次提交任务前,自动把当前线程的 MDC 复印,贴到执行任务的新线程上:
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(runnable -> {
Map<String, String> ctx = MDC.getCopyOfContextMap(); // 复印当前线程的 MDC
return () -> {
try {
if (ctx != null) MDC.setContextMap(ctx); // 贴到新线程的纸上
runnable.run();
} finally {
MDC.clear(); // 用完清空,防残留
}
};
});
return executor;
}两种办法二选一:手动拷写起来啰嗦、易漏;TaskDecorator 配一次全局生效,推荐。前提是了解原理即可,生产更多直接用链路追踪框架代替。
五、不要在日志里输出敏感信息
// ❌ 错:密码会进日志
log.info("用户登录: {}", user); // user.password 被打印
// ✅ 用脱敏
log.info("用户登录: {}", user.getUsername());或者自定义脱敏工具,用 @JsonSerialize 拦字段。
六、生产日志最佳实践
# application-prod.yml
logging:
level:
root: INFO
com.example: INFO # 自己代码 INFO
org.springframework: WARN # 框架 WARN
file:
name: /var/log/myapp/app.log
pattern:
file: "%d{...} [%thread] [%X{traceId}] %-5level %logger - %msg%n"实际输出长这样(一次 POST /orders 请求):
2026-08-14 10:30:45.100 [http-nio-8080-exec-3] [f3e8ab9c] WARN org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - Resolved exception...
2026-08-14 10:30:45.120 [http-nio-8080-exec-3] [f3e8ab9c] INFO com.team.order.filter.TraceIdFilter - 请求进入 /orders
2026-08-14 10:30:45.125 [http-nio-8080-exec-3] [f3e8ab9c] INFO com.team.order.controller.OrderController - 收到创建订单请求:userId=42
2026-08-14 10:30:45.130 [http-nio-8080-exec-3] [f3e8ab9c] INFO com.team.order.service.OrderService - 开始校验订单
2026-08-14 10:30:45.132 [http-nio-8080-exec-3] [f3e8ab9c] INFO com.team.order.mapper.OrderMapper - 执行 SQL:insert into orders...
2026-08-14 10:30:45.135 [http-nio-8080-exec-3] [f3e8ab9c] INFO com.team.order.service.OrderService - 订单创建成功,orderId=1001怎么读:每一行 = 时间 [线程] [traceId] 级别 类 - 内容。这一串全是同一个 traceId=f3e8ab9c、同一个线程——生产排查时只要 grep f3e8ab9c,整条请求从进来到返回的日志全部串起来。
哪些不会出现:你的代码里 log.debug(...)(因为 com.example: INFO,DEBUG 太低不打);Spring 框架的 INFO/DEBUG(因为 org.springframework: WARN)。只保留自己的 INFO + 框架的 WARN,日志量小又干净。
⚠️ 坑 3:生产不要输出 DEBUG,日志文件会爆炸,也会泄露信息。
七、本章小结
| 要点 | 关键 |
|---|---|
| 注解 | @Slf4j(Lombok) |
| 配置 | logback-spring.xml(支持 profile) |
| 链路追踪 | MDC + 拦截器 |
| 异步 | TaskDecorator 传播 MDC |
| 敏感信息 | 脱敏,不要直接 log 对象 |
动手练习
- 写
TraceIdFilter,在日志里看到 traceId - 加一个日志切面,记录每个请求的耗时
下一章:第 47 章:统一返回与异常处理 →