问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

通过MDC+traceId实现全局请求追踪并快速定位日志

创作时间:
作者:
@小白创作中心

通过MDC+traceId实现全局请求追踪并快速定位日志

引用
CSDN
1.
https://blog.csdn.net/qq_39354140/article/details/145107805

在大型分布式系统中,如何快速定位某次请求或方法调用的日志信息是一个常见的挑战。本文将介绍一种基于MDC(Mapped Diagnostic Context)和traceId的解决方案,通过在Spring Boot项目中实现全局请求追踪,帮助开发者快速定位日志信息。

背景

在大型分布式系统中,日志量往往非常庞大,如何在海量的日志堆栈中快速定位到某次请求或方法调用的日志信息是一个常见的挑战。本文将介绍一种基于MDC(Mapped Diagnostic Context)和traceId的解决方案,通过在Spring Boot项目中实现全局请求追踪,帮助开发者快速定位日志信息。

环境

JDK:1.8
Spring boot:2.6.1

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.36</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

方法

  1. 在服务端入口处生成一个唯一的traceId
  2. 日志中输出traceId的值
  3. 接口返回中,添加一个通用字段traceId

实现过程

1. 创建TraceFilter

通过实现一个Filter来拦截所有请求,生成唯一的TraceId并将其存入ThreadLocal和MDC。MDC是logback提供的一个拓展入口,可以向里面放入一些键值对,然后在logback日志中可以通过对应的键取出值。更多MDC的使用方法请参考MDC的官方文档:MDC(Mapped Diagnostic Context)

@Component
public class TraceFilter implements Filter {
    private static final ThreadLocal<String> ThreadLocal_TRACE_ID = new ThreadLocal<>();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化操作
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 生成唯一的 traceId
        String traceId = UUID.randomUUID().toString();
        // 将 traceId 存储到 ThreadLocal 中
        ThreadLocal_TRACE_ID.set(traceId);
        // 将 traceId 存入 MDC
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            // 请求结束后清除 MDC 中的 traceId
            MDC.remove("traceId");
        }
    }

    @Override
    public void destroy() {
        // 销毁操作
    }

    /**
     * 获取 ThreadLocal 中的 traceId
     * @return traceId
     */
    public static String getTraceId() {
        return ThreadLocal_TRACE_ID.get();
    }
}

2. 配置日志输出

在logback-spring.xml中配置日志输出格式,使其能够输出traceId:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 定义日志文件的存储路径 -->
    <property name="LOG_PATH" value="D:/data/logs/logs"/>
    <property name="LOG_FILE_NAME" value="testLog"/>

    <!-- 控制台日志输出配置 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [traceId:%X{traceId}] - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 文件日志输出配置 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [traceId:%X{traceId}] - %msg%n</pattern>
        </encoder>
        <!-- 日志文件滚动策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天生成一个日志文件 -->
            <fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 保留最近30天的日志文件 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- 日志级别配置 -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

3. 统一返回格式

为了在接口响应中返回traceId,需要定义一个统一的响应类ApiResponse:

@Data
public class ApiResponse<T> {
    private String traceId;
    private int code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("Success");
        response.setData(data);
        response.setTraceId(TraceFilter.getTraceId());
        return response;
    }
    // 其他静态工厂方法省略
}

4. 全局异常处理

为了在程序报错时也能返回traceId,需要创建统一的错误返回类ErrorResponse和全局异常处理类GlobalExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 捕获所有异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 返回 HTTP 500 状态码
    public ErrorResponse handleException(Exception ex) {
        String traceId = TraceFilter.getTraceId();
        return new ErrorResponse(traceId, 500, ex.getMessage());
    }
}
@Data
public class ErrorResponse {
    private String traceId; // 追踪 ID
    private int code;       // 错误码
    private String message; // 错误信息

    public ErrorResponse(String traceId, int code, String message) {
        this.traceId = traceId;
        this.code = code;
        this.message = message;
    }
}

测试

创建两个接口用来测试:

@RequestMapping("/api")
@RestController
public class TestController {
    @GetMapping("/test1")
    public ApiResponse<String> test1() {
        // 业务逻辑
        return ApiResponse.success("Hello, World!");
    }

    @GetMapping("/test2")
    public ApiResponse<String> test2() {
        // 业务逻辑
        int a = 1 / 0;
        return ApiResponse.success("Hello, World!");
    }
}

启动项目,分别向两个接口发送请求:

现在,可以通过前端得到的traceId快速定位到日志中对应的每次请求:

项目的结构如下:

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号