通过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快速定位到日志中对应的每次请求:
项目的结构如下:
热门推荐
轿车拖挂房车需要什么驾驶证
云南烟丝发酵与醇化
经济诈骗如何定性
不动产权证可以代替土地使用证吗
福州大学081401岩土工程专业硕士研究生招生目录
马王爷是谁?历史上流传着哪些传说?
厌氧菌和需氧菌感染区别在哪
漫游外滩,体验历史文化风貌区的Citywalk路线
感冒的过程及症状变化
公猫绝育后乱尿怎么办?原因分析与解决方案
是研究生被冷落了吗?国考近1/6职位仅限本科,而仅限研究生仅3%
HR必读:人效与人均产值的本质区别及落地应用指南
天津种植牙集采政策实施情况如何,未来种植牙价格会降低吗?
萎缩性胃炎能吃什么饼干
鸭子炖汤的最佳搭配食材,让你越吃越健康
全新养老生活,旅居时代的幸福选择
手部小动作泄露你的心理秘密
喷气增焓技术:突破低温极限的中央空调高效制热新方案
煤气罐的安装步骤是什么?这种安装如何确保使用安全?
经什么意思:从法律角度解析其深刻寓意与应用
茯苓酸枣仁茶的功效和作用 茯苓酸枣仁茶不适宜人群
笔记本电脑无响应怎么强制重启
东北酸菜真的会致癌吗?专家解读酸菜食用安全指南
什么是锂电池热失控?
亲水PTFE过滤膜的材料优势与孔径结构
缺铁性贫血患者如何正确补充铁剂?
英雄联盟电竞经理EDG阵容搭配攻略
普洱茶汤怎么也会“出油”?
总是感觉饿是什么原因引起的
降价不降质,让更多癌症患者用得起化疗药