欢迎光临
我们一直在努力

SpringBoot 系列之实现全链路日志TraceId追踪

SpringBoot 实现全链路日志TraceId追踪,排查问题更高效

在日常的后端开发和运维过程中,我们经常需要通过日志排查线上问题。但在高并发场景下,多个请求的日志会交织在一起,想要定位某个请求的完整调用链路日志,就像“大海捞针”一样困难。

而TraceId(追踪ID)就是解决这个问题的关键——它是一个请求链路的唯一标识,贯穿整个请求的所有日志输出,让我们能快速筛选出某个请求的全链路日志。本文将基于 springboot-logging-traceid 项目,手把手教你在SpringBoot项目中实现TraceId的全链路日志追踪。

一、核心原理

实现TraceId追踪的核心依赖两个关键点:

  • MDC(Mapped Diagnostic Context):日志框架(Logback/Log4j)提供的线程绑定的上下文容器,能将TraceId存入当前线程,日志输出时从MDC中取出即可。
  • 拦截器(Interceptor):在请求进入时生成TraceId并放入MDC,请求结束时清理MDC(避免内存泄漏);如果涉及异步线程,还需要手动传递MDC中的TraceId。
  • 二、项目环境准备

    1. 基础依赖(pom.xml)

    首先创建一个SpringBoot项目,引入核心依赖(已包含日志框架Logback,无需额外引入):

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.3</version>
    <relativePath/> <!– lookup parent from repository –>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot-logging-traceid</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-logging-traceid</name>
    <description>Demo project for Spring Boot</description>
    <url/>
    <licenses>
    <license/>
    </licenses>
    <developers>
    <developer/>
    </developers>
    <scm>
    <connection/>
    <developerConnection/>
    <tag/>
    <url/>
    </scm>

    <properties>
    <java.version>17</java.version>
    </properties>

    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>

    </dependencies>

    <dependencyManagement>
    <dependencies>

    </dependencies>
    </dependencyManagement>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
    <excludes>
    <exclude>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </exclude>
    </excludes>
    </configuration>
    </plugin>
    </plugins>
    </build>

    </project>

    三、核心代码实现

    1. 自定义TraceId工具类

    封装TraceId的生成、获取、移除逻辑,方便全局使用:

    package com.example.springboot.traceid.utils;

    import org.slf4j.MDC;

    /**
    * MDC工具类,用于操作MDC中的traceId
    */

    public class MdcUtil {

    public static final String TRACE_ID = "traceId";

    /**
    * 获取当前请求的traceId
    * @return traceId,如果不存在则返回null
    */

    public static String getTraceId() {
    return MDC.get(TRACE_ID);
    }

    /**
    * 设置traceId到MDC
    * @param traceId traceId
    */

    public static void setTraceId(String traceId) {
    MDC.put(TRACE_ID, traceId);
    }

    /**
    * 清理MDC中的traceId
    */

    public static void clearTraceId() {
    MDC.remove(TRACE_ID);
    }

    /**
    * 获取当前traceId,如果不存在则返回默认值
    * @param defaultValue 默认值
    * @return traceId或默认值
    */

    public static String getTraceIdOrDefault(String defaultValue) {
    String traceId = getTraceId();
    return traceId != null ? traceId : defaultValue;
    }
    }

    2. 自定义TraceId拦截器

    拦截所有HTTP请求,在请求进入时生成TraceId并放入MDC,请求结束后清理:

    package com.example.springboot.traceid.filter;

    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.slf4j.MDC;
    import org.springframework.core.Ordered;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;

    import java.io.IOException;
    import java.util.UUID;

    /**
    * TraceId过滤器,为每个请求生成唯一的traceId
    */

    @Component
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Slf4j
    public class TraceIdFilter extends OncePerRequestFilter {

    public static final String TRACE_ID = "traceId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
    FilterChain filterChain) throws ServletException, IOException {

    // 尝试从请求头中获取traceId,如果没有则生成一个新的
    String traceId = request.getHeader("X-Trace-Id");
    if (traceId == null || traceId.isEmpty()) {
    traceId = generateTraceId();
    }

    // 将traceId设置到MDC中
    MDC.put(TRACE_ID, traceId);

    try {
    // 将traceId添加到响应头中,方便追踪
    response.addHeader("X-Trace-Id", traceId);

    // 打印请求开始日志(包含traceId)
    log.info("Request started – Method: {}, URI: {}, RemoteAddr: {}",
    request.getMethod(), request.getRequestURI(), request.getRemoteAddr());

    // 继续过滤器链
    filterChain.doFilter(request, response);

    // 打印请求完成日志(包含traceId)
    log.info("Request completed – Method: {}, URI: {}, Status: {}",
    request.getMethod(), request.getRequestURI(), response.getStatus());

    } finally {
    // 请求完成后清理MDC中的traceId
    MDC.clear();
    }
    }

    private String generateTraceId() {
    return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
    }

    3. 配置日志格式(输出TraceId)

    修改Logback配置文件(logback-spring.xml),在日志格式中加入%X{traceId},让日志输出TraceId:

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!– 控制台输出Appender –>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <!– 日志格式:时间 + 线程名 + TraceId + 日志级别 + 类名 + 日志信息 –>
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} – %msg%n</pattern>
    <charset>UTF-8</charset>
    </encoder>
    </appender>

    <!– 根日志级别 –>
    <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    </root>

    <!– 自定义包的日志级别 –>
    <logger name="com.example.logging.traceid" level="INFO" additivity="false">
    <appender-ref ref="CONSOLE"/>
    </logger>
    </configuration>

    5. AOP拦截日志打印(验证TraceId)

    5. 测试接口(验证TraceId)

    编写一个测试接口,模拟业务逻辑并输出日志,验证TraceId是否贯穿整个链路:

    package com.example.logging.traceid.controller;

    import com.example.logging.traceid.util.TraceIdUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    /**
    * 日志测试接口
    */

    @Slf4j
    @RestController
    @RequestMapping("/log")
    public class LogController {

    @GetMapping("/test")
    public String testLog(String message) {
    // 模拟业务逻辑1:打印日志
    log.info("接收到请求参数,message:{}", message);
    // 调用业务方法
    doBusiness();
    // 模拟返回结果
    return "success,TraceId:" + TraceIdUtil.getTraceId();
    }

    /**
    * 模拟业务方法
    */

    private void doBusiness() {
    log.debug("执行业务逻辑(debug级别)");
    log.warn("执行业务逻辑(warn级别)");
    log.error("执行业务逻辑(error级别,仅测试)");
    }
    }

    四、异步线程的TraceId传递(扩展)

    MDC是线程绑定的,异步线程(比如@Async)会丢失TraceId,需要手动传递。

    1. 配置异步线程池

    package com.example.logging.traceid.config;

    import com.example.logging.traceid.util.TraceIdUtil;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import java.util.concurrent.Executor;
    import java.util.concurrent.ThreadPoolExecutor;

    /**
    * 异步线程池配置,传递TraceId
    */

    @Configuration
    @EnableAsync
    public class AsyncConfig {

    @Bean("asyncExecutor")
    public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // 核心线程数
    executor.setCorePoolSize(5);
    // 最大线程数
    executor.setMaxPoolSize(10);
    // 队列大小
    executor.setQueueCapacity(20);
    // 线程前缀
    executor.setThreadNamePrefix("async-thread-");
    // 拒绝策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    // 初始化
    executor.initialize();

    // 包装线程池,传递MDC中的TraceId
    return new ThreadPoolTaskExecutor() {
    @Override
    public <T> java.util.concurrent.Future<T> submit(java.util.concurrent.Callable<T> task) {
    // 获取当前线程的TraceId
    String traceId = MdcUtil.getTraceId();
    return super.submit(() -> {
    try {
    // 将TraceId放入异步线程的MDC
    MdcUtil.setTraceId(traceId);
    return task.call();
    } finally {
    // 清理异步线程的TraceId
    MdcUtil.clearTraceId();
    }
    });
    }

    @Override
    public void execute(Runnable task) {
    String traceId = MdcUtil.getTraceId();
    super.execute(() -> {
    try {
    MdcUtil.setTraceId(traceId);
    task.run();
    } finally {
    MdcUtil.clearTraceId();
    }
    });
    }
    };
    }
    }

    2. 异步方法测试

    在LogController中添加异步方法:

    /**
    * 异步方法测试
    */

    @Async("asyncExecutor")
    public void asyncMethod() {
    log.info("异步方法执行中,TraceId:{}", MdcUtil.getTraceId());
    }

    // 在testLog方法中调用异步方法
    @GetMapping("/test")
    public String testLog(String message) {
    // 模拟业务逻辑1:打印日志
    log.info("接收到请求参数,message:{}", message);
    // 调用异步方法
    ExecutorService executor = Executors.newFixedThreadPool(2);

    log.info("主线程日志 – traceId: {}", MdcUtil.getTraceId());
    String traceId = MdcUtil.getTraceId();

    // 解决方案:使用包装器包装任务
    executor.submit(() -> {
    MdcUtil.setTraceId(traceId);
    log.info("子线程日志 – traceId应该可见: {}", MdcUtil.getTraceId());

    // 嵌套任务也要设置traceId
    CompletableFuture.runAsync(() -> {
    MdcUtil.setTraceId(traceId);
    log.info("嵌套异步任务日志 – traceId应该可见: {}", MdcUtil.getTraceId());
    });
    });
    // 模拟返回结果
    return "success,TraceId:" + MdcUtil.getTraceId();
    }

    编写测试类,验证线程池情况手动加上traceId:

    package com.example.springboot.traceid;

    import com.example.springboot.traceid.utils.MdcUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;

    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    /**
    * 异步环境下traceId传递测试
    */

    @SpringBootTest
    @Slf4j
    public class AsyncTraceIdTest {

    /**
    * 测试1:演示问题 – 普通线程池中traceId丢失
    */

    @Test
    public void testProblemWithoutMdcWrapper() throws Exception {
    log.info("测试1:演示普通线程池中traceId丢失问题");

    ExecutorService executor = Executors.newFixedThreadPool(2);

    // 在主线程设置traceId
    MdcUtil.setTraceId("test-trace-id-123");
    log.info("主线程日志 traceId: {}", MdcUtil.getTraceId());

    // 问题:直接提交任务,traceId不会传递
    executor.submit(() -> {
    log.info("子线程日志 – traceId: {}", MdcUtil.getTraceId());
    }).get();

    executor.shutdown();
    }

    /**
    * 测试2:手动加traceId
    */

    @Test
    public void testWithMdcWrapper() throws Exception {
    log.info("测试2:手动加traceId ");

    ExecutorService executor = Executors.newFixedThreadPool(2);

    // 在主线程设置traceId
    MdcUtil.setTraceId("test-trace-id-456");
    log.info("主线程日志 – traceId: {}", MdcUtil.getTraceId());
    String traceId = MdcUtil.getTraceId();

    // 解决方案:使用包装器包装任务
    executor.submit(() -> {
    MdcUtil.setTraceId(traceId);
    log.info("子线程日志 – traceId: {}", MdcUtil.getTraceId());

    // 嵌套任务也会继承traceId
    CompletableFuture.runAsync(() -> {
    MdcUtil.setTraceId(traceId);
    log.info("嵌套异步任务日志 – traceId: {}", MdcUtil.getTraceId());
    });
    }).get();

    Thread.sleep(100); // 等待嵌套任务完成

    executor.shutdown();
    }

    }

    五、测试验证

    1. 启动项目

    运行SpringBoot主类(自动生成,无需额外编写):

    package com.example.logging.traceid;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class SpringbootLoggingTraceidApplication {
    public static void main(String[] args) {
    SpringApplication.run(SpringbootLoggingTraceidApplication.class, args);
    }
    }

    2. 调用接口

    通过Postman/浏览器访问:http://localhost:8080/log/test?message=hello-traceid

    3. 查看日志输出

    控制台会输出类似如下日志,所有日志都包含同一个TraceId(比如f8e7a9876543210abcdef):

    2024-05-20 15:30:00.123 [http-nio-8080-exec-1] [f8e7a9876543210abcdef] INFO c.e.l.t.interceptor.TraceIdInterceptor – 请求开始,TraceId:f8e7a9876543210abcdef,请求路径:/log/test
    2024-05-20 15:30:00.124 [http-nio-8080-exec-1] [f8e7a9876543210abcdef] INFO c.e.l.t.controller.LogController – 接收到请求参数,message:hello-traceid
    2024-05-20 15:30:00.125 [async-thread-1] [f8e7a9876543210abcdef] INFO c.e.l.t.controller.LogController – 异步方法执行中,TraceId:f8e7a9876543210abcdef
    2024-05-20 15:30:00.126 [http-nio-8080-exec-1] [f8e7a9876543210abcdef] DEBUG c.e.l.t.controller.LogController – 执行业务逻辑(debug级别)
    2024-05-20 15:30:00.127 [http-nio-8080-exec-1] [f8e7a9876543210abcdef] WARN c.e.l.t.controller.LogController – 执行业务逻辑(warn级别)
    2024-05-20 15:30:00.128 [http-nio-8080-exec-1] [f8e7a9876543210abcdef] ERROR c.e.l.t.controller.LogController – 执行业务逻辑(error级别,仅测试)
    2024-05-20 15:30:00.129 [http-nio-8080-exec-1] [f8e7a9876543210abcdef] INFO c.e.l.t.interceptor.TraceIdInterceptor – 请求结束,TraceId:f8e7a9876543210abcdef,响应状态码:200

    可以看到:

    • 同一个请求的所有日志(包括主线程和异步线程)都包含相同的TraceId;
    • 响应结果中也能返回TraceId,方便前端关联;
    • 请求结束后TraceId被清理,无内存泄漏风险。 在这里插入图片描述

    六、总结

    通过以上步骤,我们在SpringBoot项目中实现了TraceId的全链路日志追踪,核心要点:

  • 利用拦截器在请求入口生成TraceId并放入MDC;
  • 配置日志格式输出TraceId,让日志带上唯一标识;
  • 异步线程需手动传递MDC中的TraceId,避免丢失;
  • 请求结束后清理MDC,防止内存泄漏。
  • 这个方案能极大提升日志排查效率,尤其在微服务、高并发场景下,通过TraceId可以快速定位单个请求的全链路日志,是后端开发中非常实用的基础能力。

    完整代码可参考:springboot-logging-traceid

    赞(0)
    未经允许不得转载:171主机测评 » SpringBoot 系列之实现全链路日志TraceId追踪
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址