前言
在分布式或并发环境下,当用户反馈接口报错时,单靠时间线查找日志无异于大海捞针。由于请求是并发的,不同请求的日志会交错在一起,我们很难还原某个特定请求的完整执行链路。
为了解决这个问题,我们需要引入 Trace ID(链路追踪标识) 。
实现思路
- 唯一标识:为每一个进入系统的请求分配一个全局唯一的 ID。
- 全链路串联:通过 Trace ID,可以将从 Controller 到 Service、再到 DAO 甚至异步线程的所有日志汇聚在一起。
- 快速排查:前端在报错时只需提供这个 ID,开发者即可在成千上万行日志中瞬间定位到问题的始末。
我们通常利用日志框架提供的 MDC(Mapped Diagnostic Context) 机制来实现。在请求进入系统时(如拦截器或过滤器中)生成 ID 并存入 MDC,后续该请求触发的所有日志都会自动携带此标识,无需手动修改原有的 Log 代码。
MDC
MDC 是一个"基于线程的日志上下文容器",它的核心原理是(以 loggack 为例):
java
public class MDC {
private static final ThreadLocal<Map<String, String>> contextMap;
}
每一个请求都是独立的一份 MDC 容器,并发请求不会互相污染。
常用方法
- 设置:
MDC.put("traceId", traceId); - 清理:
MDC.remove("traceId");或MDC.clear()
日志配置
通过 MDC 配置的内容想要在日志中打印,还需要我们在日志文件中进行配置,关键配置如下:
xml
<pattern>
[%X{traceId:--}] %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
</pattern>
%X{key}: key 为在 MDC 中设定的key,这样在打印日志时,就会从 MDC 工具中相应的值了。%X{traceId:--}::-后面是默认值,这里表示 traceId 不存在时打印-。(注意":-"后面还有个"-")
请求链路追踪过滤器
配置文件
使用配置文件,我们可以很方便的控制链路追踪的启停,同时更规范的管理使用到的 key:
java
@Data
@ConfigurationProperties(prefix = "x-polaris.web")
public class PolarisWebProperties {
/**
* 链路追踪配置
*/
private Trace trace = new Trace();
@Data
public static class Trace {
/**
* 是否开启链路追踪,默认 true
*/
private boolean enabled = true;
/**
* 链路追踪 ID 的 Header 名称,默认 X-Trace-Id
*/
private String headerName = "X-Trace-Id";
/**
* MDC 中的 Key 名称,默认 traceId
*/
private String mdcKey = "traceId";
}
}
过滤器
java
@RequiredArgsConstructor
public class TraceIdFilter extends OncePerRequestFilter {
private final PolarisWebProperties properties;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
PolarisWebProperties.Trace traceConfig = properties.getTrace();
if (!traceConfig.isEnabled()) {
// 未启用时,直接放行
filterChain.doFilter(request, response);
return;
}
// 获取请求头已有的 traceId,如果不存在才新建一个
String traceId = request.getHeader(traceConfig.getHeaderName());
if (!StringUtils.hasText(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
try {
// 记录 traceId 到 MDC,方便后续日志打印
MDC.put(traceConfig.getMdcKey(), traceId);
response.setHeader(traceConfig.getHeaderName(), traceId);
filterChain.doFilter(request, response);
} finally {
// 移除 MDC 中的 traceId,防止内存泄漏
MDC.remove(traceConfig.getMdcKey());
}
}
}
日志配置(logback)
下面是使用 logback 的简单的日志配置:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定义日志格式,增加 Trace ID [%X{traceId}] -->
<property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr([%X{traceId:--}]){yellow} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 引入 Spring Boot 官方预定义的 Logback 配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
简单测试
现在我们启动项目,此时可以看到控制台打印了 [-],我们请求接口(接口中有日志打印的话)就能看到对应的 traceId 了,类似 [d7dc27eba473456cafcf6dfe1ff2836f] 。
问题
MDC 是线程独立的,那么如果请求中开启了新的线程,又怎么处理呢??