通过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>
方法
- 在服务端入口处生成一个唯一的traceId
- 日志中输出traceId的值
- 接口返回中,添加一个通用字段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快速定位到日志中对应的每次请求:
项目的结构如下:
热门推荐
不拘一格超前布局 给足青年创新底气
兰州砸重金“抢人”推行大学生留兰倍增计划
眼周烦恼:起皮红肿干皱痒,破解之道与日常养护
人形机器人第一股遭遇"滑铁卢",优必选打响股价保卫战
词彙精選:close的用法和辨析大綱
蓝牙开启真的费电吗?揭秘手机蓝牙功耗真相!
《CDN深度解析:工作原理、部署方法与安全防护》
感冒做什么运动恢复快
老人脸浮肿是什么原因
微软官方U盘制作工具重装Windows11系统教程
台儿庄守城指挥官康法如:抗日英雄的传奇与悲剧
服务器如何正确关机?
5种常用的胃黏膜保护剂,应如何正确选用?一文搞定
生活中常见轻微违法行为及其法律后果
为什么鹅蛋更大,却不像鸡蛋、鸭蛋那么受欢迎?
膳食纤维,你吃够了吗?该吃哪些食物补充?
科普|四季话“桑树”
吉他音阶入门教程:从基础概念到实战练习
银行的教育储蓄存款有什么优惠政策?
职场关系:如何达成共识?
美国买房的10个关键步骤全攻略
大陆居民如何前往台湾?飞机、船只多种交通方式全解析
脑机接口技术带来了哪些伦理思考?未来的人类将变化几何!
修改姓氏需要什么流程
内行人买大米,不只看价格,认准米袋“3行字”,挑到优质好米
Pascal 编程语言:从辉煌到传承
有人轻生怎么劝说
为什么IT招生策略需要根据市场变化调整?
眼镜检测:项目、标准与流程全解析
安德鲁·杰克逊:人民总统和民粹主义的崛起