阿里TTL+Log4j2+MDC实现轻量级日志链路追踪

参考

阿里TTL:https://github.com/alibaba/transmittable-thread-local

TLog:https://tlog.yomahub.com/

背景

image

推荐阅读:https://tlog.yomahub.com/pages/5b7bd2/,本篇文章也是看了TLog的官方文档和相关源码而产生的。

在微服务架构中,由于线程池复用、异步调用等机制的存在,传统的线程级日志标识(如ThreadLocal)会导致请求链路断裂。例如,当主线程将任务提交到线程池时,子线程无法自动继承主线程的上下文信息,使得日志中的traceId丢失。

核心组件原理

MDC机制

Log4j2Mapped Diagnostic Context(MDC)通过ThreadLocal存储线程级上下文数据。例如,在HTTP请求进入时,通过拦截器将traceId存入MDC,日志模板中通过%X{traceId}动态替换值。

TransmittableThreadLocal(TTL)

阿里的TTL组件解决了线程池场景下的上下文传递问题。通过装饰线程池,TTL在任务提交时自动拷贝父线程的上下文到子线程,并在任务结束后清理副本,确保多级线程池调用链路完整。

日志模板配置

log4j2.xml中配置PatternLayout,添加%X{traceId}占位符即可实现日志标识嵌入。

1
<Property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%t] %-5level %logger{36} - %msg%n"/>

实现方案详解

1、TraceId生成

  • 简单场景:使用UUID生成唯一标识(代码示例)
  • 分布式场景:建议采用雪花算法(Snowflake),结合机器ID和时间戳生成全局唯一ID

2、组件集成

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Log4j2核心依赖 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
</dependency>

<!-- 阿里TTL -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>

如果是Springboot使用Log4j2要排除默认日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 排除默认日志 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--排除默认log-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

3、链路上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 基于TransmittableThreadLocal实现线程池安全的TraceID传递
*
* @author wnhyang
* @date 2025/3/3
**/
public class TraceContext {

private static final TransmittableThreadLocal<String> TRACE_ID = new TransmittableThreadLocal<>();

/**
* 设置TraceID,并同步到Log4j2的MDC
*/
public static void setTraceId(String traceId) {
TRACE_ID.set(traceId);
}

public static String getTraceId() {
return TRACE_ID.get();
}

public static void clear() {
TRACE_ID.remove();
}

public static String generateTraceId() {
return IdUtil.simpleUUID();
}
}

4、生成并传递TraceId

需要注意这里只是写了作为链路源头生成TraceId和向指定的下游传递的示例。

然而在实际情况下通常会比较复杂,因为源头通常是不可知的(或是说不是那么清楚的),所以要包含几个步骤:1、检查上游是否有TraceId传递;2、生成唯一TraceId;3、传递TraceId

这些在TLog中都以插件的形式实现,可以参考,不过TLog好像没有兼容JDK17,可以参考自己实现。另外正如TLog官方所讲:“当然分布式追踪系统是一个最终的解决方案,如果您的公司已经上了分布式追踪系统,那TLog并不适用。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @author wnhyang
* @date 2025/3/3
**/
@Slf4j
public class TraceFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 生成并设置TraceID
String traceId = TraceContext.generateTraceId();
TraceContext.setTraceId(traceId);
MDC.put(TraceConstants.TRACE_KEY, traceId);

// 透传TraceID到下游(可选)
response.setHeader(TraceConstants.TRACE_HEADER_KEY, traceId);

filterChain.doFilter(request, response);
} finally {
// 必须清理上下文
TraceContext.clear();
MDC.remove(TraceConstants.TRACE_KEY);
}
}

}

5、装饰线程池

链路上下文中已经使用了TTL存放TraceId, 所以这里就是装饰线程池。

当然方式也不至这里的一种,还有如使用TtlExecutors直接包装线程池等方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @author wnhyang
* @date 2025/3/3
**/
public class ThreadPoolFactory {
public static ThreadPoolTaskExecutor createExecutor(int coreSize,
int maxSize,
int queueCapacity,
String threadNamePrefix,
RejectedExecutionHandler rejectedExecutionHandler) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(coreSize);
executor.setMaxPoolSize(maxSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(rejectedExecutionHandler);
executor.setTaskDecorator(getTraceContextDecorator());
executor.initialize();
return executor;
}

private static TaskDecorator getTraceContextDecorator() {
return runnable -> TtlRunnable.get(() -> {
try {
MDC.put(TraceConstants.TRACE_KEY, TraceContext.getTraceId());
runnable.run();
} finally {
MDC.clear();
}
});
}
}

6、Log4j2配置

配置上前面MDC.puttraceId即可,如:[%X{traceId}]

1
<Property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%t] %-5level %logger{36} - %msg%n"/>

效果展示

这里只有复用线程池的效果展示。。。

image

可以看到链路号在多线程下仍然是一致的,而且多个链路下不会污染。

关于项目

https://github.com/wnhyang/coolGuard

进度

最近主要有两点更新:1、决策流程优化,之前太过依赖LiteFlow了,最近的更新对这块进行了改善;2、项目组织结构,这个一直是我不满意的地方,而且历史提交中这样的大改已经不是一次两次的了。

之后应该要考虑分支管理了,在开源项目完善的同时新增Pro版,增加一些更丰富的内容。

image

当然上面的测试不是很严谨,不过也能说明一些问题。而且项目开发期间一直有向性能、项目架构等等方面考虑。

image

推荐文章

规则引擎可以应用于哪些系统,用户画像、触达、风控、推荐、监控...

基于规则引擎的风控决策系统介绍与演示

风控系统之规则重复触发

LiteFlow上下文与组件设计,数据依赖梳理

交易事件的生命周期,事前事中事后风控,结果通知/回调

策略/规则篇

风控系统之普通规则条件,使用LiteFlow实现

风控系统之通用规则条件设计,算术单元/逻辑单元/函数式接口

LiteFlow决策系统的策略模式,顺序、最坏、投票、权重

指标篇

风控系统指标计算/特征提取分析与实现01,Redis、Zset、模版方法

基于LiteFlow的风控系统指标版本控制

风控系统指标版本管理,前端实现

风控系统之指标回溯,历史数据重跑

业务链指标,用户行为模式识别,埋点系统

数据篇

风控系统之数据服务,名单、标签、IP、设备、地理信息、征信等

GeoHash处理经纬度,降维,空间填充曲线