Skip to content
第 46 章 后端 ⏱ 8 分钟阅读

第 46 章:日志与链路追踪 ​

学习目标 ​

  • 用 Logback 输出结构化日志
  • 用 MDC 实现请求链路追踪
  • 学会日志级别与生产配置

一、Logback 基础 ​

Spring Boot 默认用 Logback,无需配置。

java
@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:

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 完整定义(过滤器) ​

java
// 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 自动完成的:

  1. 组件扫描:@SpringBootApplication 扫自己包及子包,@Component 标注的 TraceIdFilter 被找到 → 成为容器里的 bean。
  2. 自动注册成过滤器: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;
    }
}
xml
<!-- 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 了。要解决,就得把主线程纸上的内容复印一份贴到新线程纸上。

办法一:手动拷(每个任务里写)

java
Runnable task = () -> {
    MDC.put("traceId", traceId);   // 拷到新线程的纸上
    try {
        doWork();
    } finally {
        MDC.remove("traceId");     // 用完清掉
    }
};
new Thread(task).start();

办法二:TaskDecorator(线程池自动复印,一处配置全生效)

思路:给线程池设一个"包装器",每次提交任务前,自动把当前线程的 MDC 复印,贴到执行任务的新线程上:

java
@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 配一次全局生效,推荐。前提是了解原理即可,生产更多直接用链路追踪框架代替。

五、不要在日志里输出敏感信息 ​

java
// ❌ 错:密码会进日志
log.info("用户登录: {}", user);  // user.password 被打印

// ✅ 用脱敏
log.info("用户登录: {}", user.getUsername());

或者自定义脱敏工具,用 @JsonSerialize 拦字段。

六、生产日志最佳实践 ​

yaml
# 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 对象

动手练习 ​

  1. 写 TraceIdFilter,在日志里看到 traceId
  2. 加一个日志切面,记录每个请求的耗时

下一章:第 47 章:统一返回与异常处理 →

本站基于 VitePress 构建 · 由 StackHub 团队维护