日志链路追踪 ID 实现
日志链路追踪 ID 实现,从 Web 请求到定时任务的全链路覆盖。
在分布式系统和微服务架构中,日志追踪是排查问题的关键手段。通过统一的追踪 ID(Trace ID),我们可以将分散在不同服务、不同组件中的日志串联起来,形成完整的调用链路。本文将介绍如何在 Java 项目中实现全场景的日志追踪 ID 管理,覆盖 Web 请求、XXL-Job 任务和 Spring 定时任务。
一、核心工具类设计
首先我们需要一个核心工具类来管理追踪 ID,基于 SLF4J 的 MDC(Mapped Diagnostic Context)实现上下文传递:
import cn.hutool.core.util.IdUtil;
import org.slf4j.MDC;
/**
* 日志追踪工具类
* 封装了MDC中TraceId的创建、设置、获取和清除操作
*/
public class MDCTraceUtils {
/**
* 追踪id的名称
* 建议在日志格式中配置此变量,如:%X{TID}
*/
public static final String KEY_TRACE_ID = "TID";
/**
* filter的优先级,值越低越优先
*/
public static final int FILTER_ORDER = -1;
/**
* 创建traceId并赋值MDC
* 适用于需要新建追踪ID的场景
*/
public static void addTraceId() {
MDC.put(KEY_TRACE_ID, createTraceId());
}
/**
* 赋值MDC(下游dubbo或fegin赋值)
* 适用于从上游系统传递追踪ID的场景(如服务间调用)
*/
public static void putTraceId(String traceId) {
MDC.put(KEY_TRACE_ID, traceId);
}
/**
* 获取MDC中的traceId值
* 可用于在需要时将追踪ID传递给下游服务
*/
public static String getTraceId() {
return MDC.get(KEY_TRACE_ID);
}
/**
* 清除MDC的值
* 必须在任务结束时调用,避免线程复用导致的上下文污染
*/
public static void removeTraceId() {
MDC.remove(KEY_TRACE_ID);
}
/**
* 创建traceId
*/
public static String createTraceId() {
return IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase();
}
}
二、Web 请求链路实现
对于 Web 应用,我们通过过滤器在请求入口处统一处理追踪 ID:
/**
* Web请求日志链路追踪过滤器
* 负责在HTTP请求进入时创建或传递TraceId,并在请求结束时清理
*/
@Component
@Order(value = MDCTraceUtils.FILTER_ORDER)
@ConditionalOnClass(value = {HttpServletRequest.class, OncePerRequestFilter.class})
public class WebTraceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws IOException, ServletException {
try {
// 优先从请求头获取TraceId,实现跨服务追踪
String traceId = request.getHeader(MDCTraceUtils.KEY_TRACE_ID);
if (StringUtils.isEmpty(traceId)) {
// 若请求头中没有,则创建新的TraceId
MDCTraceUtils.addTraceId();
} else {
// 若有则复用,保持链路连贯性
MDCTraceUtils.putTraceId(traceId);
}
// 继续执行过滤器链
filterChain.doFilter(request, response);
} finally {
// 无论请求处理结果如何,最终都要清除TraceId
// 避免Tomcat线程池复用导致的上下文污染
MDCTraceUtils.removeTraceId();
}
}
}
实现要点:
- 使用
OncePerRequestFilter确保每个请求只处理一次 - 通过
@Order设置最高优先级,确保在业务逻辑前执行 - 支持从请求头获取上游传递的 TraceId,实现分布式追踪
- 采用 try-finally 结构保证清理操作一定会执行
三、XXL-Job 任务追踪实现
对于 XXL-Job 分布式任务,通过 AOP 实现追踪 ID 管理:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* XXL-Job任务日志链路AOP
* 为定时任务添加独立的TraceId,便于任务执行日志的追踪
*/
@Aspect
@Component
public class XxlJobTraceAop {
/**
* 在XXL-Job任务执行前添加TraceId
* 切点定义为所有标注@XxlJob注解的方法
*/
@Before("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
public void beforeMethod() {
// 往当前线程中增加TID
// 注意:XXL-Job的执行线程由任务调度器管理
// 实际应用中可根据需要在任务执行完成后手动调用removeTraceId()
MDCTraceUtils.addTraceId();
}
}
注意事项:
- XXL-Job 的任务执行线程来自线程池,具有复用性
- 若任务执行完成后需要清除 TraceId,可添加 @After 注解的方法调用 removeTraceId ()
- 独立的 TraceId 可以清晰区分不同任务实例的执行日志
四、Spring Scheduled 定时任务追踪
对于 Spring 自带的 @Scheduled 定时任务,同样通过 AOP 实现:
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* Spring定时任务日志链路AOP
* 为@Scheduled注解的定时任务提供完整的TraceId生命周期管理
*/
@Aspect
@Component
public class ScheduledTraceAop {
/**
* 任务执行前创建TraceId
* 切点精准匹配所有标注@Scheduled注解的方法
*/
@Before("execution(* *(..)) && @annotation(org.springframework.scheduling.annotation.Scheduled)")
public void beforeMethod() {
// 往当前线程中增加TID,不会自动销毁,会保留在当前线程的MDC中,直到显式删除或线程结束
MDCTraceUtils.addTraceId();
}
/**
* 任务执行后清除TraceId
* 确保线程池复用不会导致TraceId污染
*/
@After("@annotation(org.springframework.scheduling.annotation.Scheduled)")
public void afterMethod() {
MDCTraceUtils.removeTraceId(); // 显式删除,避免线程池复用带来的脏数据
}
}
设计考量:
- 采用 @Before 和 @After 注解实现 TraceId 的自动创建与清理
- 明确的生命周期管理避免了线程池复用带来的上下文污染
- 精准的切点表达式确保只对定时任务生效
五、Dubbo 服务的 TraceId 传递原理
Dubbo 提供了过滤器(Filter)机制,允许我们在服务调用前后进行自定义处理。通过实现Filter接口,我们可以:
- 服务消费者:在发起调用前将当前线程的 TraceId 放入请求上下文
- 服务提供者:在接收请求时从上下文提取 TraceId 并设置到本地 MDC
- 确保调用完成后清理上下文,避免线程污染
1. Dubbo 消费者过滤器(传递 TraceId)
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Dubbo消费者过滤器:将当前线程的TraceId传递到服务提供者
*/
@Activate(group = CommonConstants.CONSUMER, order = -1000) // 消费者端激活,优先级高于业务过滤器
public class DubboConsumerTraceFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(DubboConsumerTraceFilter.class);
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
// 获取当前线程的TraceId
String traceId = MDCTraceUtils.getTraceId();
if (traceId != null) {
// 将TraceId放入Dubbo调用上下文(attachment)
RpcContext.getContext().setAttachment(MDCTraceUtils.KEY_TRACE_ID, traceId);
logger.debug("Dubbo消费者传递TraceId: {}", traceId);
}
// 执行远程调用
return invoker.invoke(invocation);
} finally {
// 消费者端无需清除MDC,由上游调用方(如Web过滤器)负责
}
}
}
2. Dubbo 提供者过滤器(接收并设置 TraceId)
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Dubbo提供者过滤器:从调用上下文提取TraceId并设置到本地MDC
*/
@Activate(group = CommonConstants.PROVIDER, order = -1000) // 提供者端激活,优先级最高
public class DubboProviderTraceFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(DubboProviderTraceFilter.class);
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
// 从Dubbo调用上下文获取TraceId
String traceId = RpcContext.getContext().getAttachment(MDCTraceUtils.KEY_TRACE_ID);
if (traceId != null) {
// 设置到本地MDC
MDCTraceUtils.putTraceId(traceId);
logger.debug("Dubbo提供者接收TraceId: {}", traceId);
} else {
// 若没有传递TraceId,则创建新的(适用于直接调用服务的场景)
MDCTraceUtils.addTraceId();
logger.debug("Dubbo提供者创建新的TraceId: {}", MDCTraceUtils.getTraceId());
}
// 执行服务方法
return invoker.invoke(invocation);
} finally {
// 清除MDC,避免Dubbo线程池复用导致的上下文污染
MDCTraceUtils.removeTraceId();
}
}
}
3. 配置 Dubbo 过滤器
在resources/META-INF/dubbo目录下创建org.apache.dubbo.rpc.Filter文件,注册我们的过滤器:
# 格式:过滤器名称=全类名
dubboConsumerTraceFilter=com.yourpackage.filter.DubboConsumerTraceFilter
dubboProviderTraceFilter=com.yourpackage.filter.DubboProviderTraceFilter
4.实现要点说明
- 过滤器激活策略:
- 消费者过滤器仅在
CONSUMER分组激活 - 提供者过滤器仅在
PROVIDER分组激活 - 通过
order = -1000确保在其他过滤器之前执行,保证 TraceId 尽早设置
- 消费者过滤器仅在
- 上下文传递机制:
- 利用 Dubbo 的
RpcContext作为 TraceId 的传递载体 attachment是 Dubbo 专门用于传递附加信息的键值对集合
- 利用 Dubbo 的
- 异常处理:
- 提供者端使用
try-finally确保 TraceId 一定会被清除 - 消费者端不需要清除 MDC,因为它的生命周期由上游(如 Web 请求过滤器)管理
- 提供者端使用
- 兼容性考虑:
- 支持服务提供者被直接调用(无上游 TraceId)的场景,自动创建新 ID
- 与原有 Web / 定时任务的 TraceId 机制无缝衔接
六、日志配置建议
为了使 TraceId 在日志中生效,需要在日志配置文件中添加 TID 变量,以 logback 为例:
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%X{TID}] - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %msg%n</pattern>
</encoder>
</appender>
总结
本方案通过工具类 + 过滤器 + AOP 的组合方式,实现了以下场景的日志追踪 ID 管理:
- Web 请求:从 HTTP 请求进入到响应完成的全链路追踪
- XXL-Job 任务:分布式任务的独立追踪 ID
- Spring 定时任务:内置定时任务的完整生命周期追踪
- Dubbo 服务的 TraceId 传递原理
通过统一的 TraceId,我们可以在日志系统中快速定位和串联相关日志,极大提升问题排查效率。在实际应用中,还可以扩展到 RPC 调用、消息队列等场景,实现全链路追踪。

4432

被折叠的 条评论
为什么被折叠?



