14 KiB
任务:TraceId 链路追踪与上下文传播能力
你现在需要为我的 Java 单体项目实现一套“统一 TraceId 链路追踪基础设施”。
一、项目背景
当前项目是 Java 单体应用,技术栈:
- JDK 8
- Spring Boot 2.5.x
- Spring MVC
- Logback / SLF4J
- MySQL
- Redis
- Quartz
- MQ
- Netty
- MQTT
- 后续会接入 Python AI 服务、LLM、微服务、OpenTelemetry
项目未来会逐步发展为:
单体应用
↓
IoT + SaaS + AI
↓
微服务
↓
OpenTelemetry / 分布式链路追踪
因此这次实现不能只是简单地增加一个 MDC 字段,而要设计成可长期演进的基础框架。
二、核心目标
实现统一的:
TraceContext
核心字段至少包括:
traceId
tenantId
userId
其中当前阶段最重要的是:
traceId
核心原则:
一次完整业务执行链使用同一个 traceId。
例如:
前端请求
↓
Controller
↓
Service
↓
线程池
↓
MQ
↓
MQ Consumer
↓
AI 服务
↓
数据库
↓
通知
如果它们属于同一次业务执行,则全部使用:
traceId = A
三、TraceId 生命周期规则
必须严格遵循以下规则。
1. 有上游 TraceId
优先继承。
例如 HTTP:
X-Trace-Id: 01KABC...
后端收到后:
TraceId = 01KABC...
不要重新生成。
2. 没有上游 TraceId
自动创建新的 TraceId。
例如:
浏览器
↓
HTTP
↓
Java
如果 HTTP 没有:
X-Trace-Id
则 Java 自动生成。
3. 定时任务没有上游 TraceId
Quartz / @Scheduled 等任务启动时自动生成:
traceId
例如:
traceId=T202609150001
整个任务执行过程保持相同 TraceId。
四、TraceId 生成器
设计独立:
TraceIdGenerator
不要把 TraceId 生成逻辑写死在 Filter 中。
要求:
TraceContext
TraceIdGenerator
职责分离。
优先考虑 ULID 或项目中已有的高质量 ID 生成方案。
要求:
- 唯一
- 高并发安全
- 尽量时间有序
- 适合日志检索
- 不依赖数据库
- 后续可以替换实现
五、TraceContext 设计
创建统一:
TraceContext
例如:
TraceContext
├── getTraceId()
├── setTraceId()
├── getOrCreateTraceId()
├── set()
├── get()
├── clear()
└── snapshot()
不要让业务代码直接到处:
MDC.put("traceId", ...)
业务代码应该依赖:
TraceContext
MDC 只是 TraceContext 的一种底层日志传播实现。
核心原则:
TraceContext ≠ MDC
而应该是:
TraceContext
↓
MDC
HTTP Header
MQ Header
ThreadPool
Future
六、Web 请求
实现 Spring MVC Filter。
建议:
OncePerRequestFilter
功能:
- 从 HTTP Header 获取:
X-Trace-Id
- 如果存在且合法:
继续使用
- 如果不存在:
生成 TraceId
- 放入:
TraceContext
MDC
- 将 TraceId 写入 HTTP Response Header:
X-Trace-Id: 01KABC...
- 请求结束后必须清理:
MDC
TraceContext
防止 Tomcat 线程池复用造成 TraceId 污染。
七、日志输出
修改 Logback 日志格式,使所有日志自动包含:
traceId
例如:
2026-09-15 11:20:31.123 INFO
[http-nio-8080-exec-5]
traceId=01KABC123
DeviceService
-
查询设备成功 deviceId=DEV001
要求:
- 控制台日志包含 traceId
- 普通文件日志包含 traceId
- error 日志包含 traceId
- 不要求业务代码修改每一条 log.info()
- 使用 MDC 自动完成
同时考虑 JSON 日志扩展能力。
八、线程池上下文传播
这是本任务的重点。
普通:
ThreadPoolExecutor
或者:
CompletableFuture
不能保证 MDC 自动传播。
必须实现统一的上下文传播机制。
例如:
TraceContext
↓
snapshot
↓
Runnable / Callable
↓
restore
↓
执行
↓
clear
要求实现类似:
TraceContextRunnable
TraceContextCallable
TraceableExecutor
或者使用 Spring 提供的:
TaskDecorator
如果项目使用 Spring ThreadPoolTaskExecutor,优先通过:
TaskDecorator
实现。
目标:
Web线程
traceId=A
↓
线程池
traceId=A
例如:
executor.execute(() -> {
log.info("异步处理");
});
日志必须仍然是:
traceId=A
九、CompletableFuture
重点处理:
CompletableFuture.runAsync(...)
CompletableFuture.supplyAsync(...)
thenApply(...)
thenAccept(...)
thenRun(...)
分析项目中现有异步代码。
不要简单粗暴地修改所有业务代码。
优先通过统一线程池:
TraceableExecutor
解决。
如果无法完全覆盖 CompletableFuture 的默认线程池,需要明确指出风险,并提供统一解决方案。
禁止让业务代码到处手动:
MDC.put(...)
MDC.clear(...)
十、MQ TraceId 传播
这是第二个重点。
当 Java 发送 MQ 消息时:
必须把当前:
traceId
写入 MQ Message Header / Properties。
例如:
traceId=A
发送:
MQ Message
├── Header
│ └── traceId=A
└── Body
Consumer 收到消息:
MQ Header
↓
读取 traceId
↓
TraceContext.setTraceId()
↓
MDC
↓
业务处理
这样:
Web
traceId=A
↓
MQ Producer
traceId=A
↓
MQ
traceId=A
↓
Consumer
traceId=A
如果 MQ 消息没有 TraceId:
Consumer
↓
自动生成新的 TraceId
但正常情况下必须继承 Producer 的 TraceId。
十一、MQ 不同消息中间件
先检查当前项目实际使用的 MQ。
不要凭空假设。
如果项目目前只有一种 MQ:
优先实现当前实际使用的 MQ。
同时将设计抽象成:
TraceMessageInterceptor
未来可以支持:
RabbitMQ
Kafka
RocketMQ
Redis Stream
其他 MQ
不要为了扩展性一次性实现大量没有使用的 MQ。
十二、HTTP 调用链
如果 Java 单体项目会调用其他服务:
例如:
Java
↓ HTTP
Python AI
必须自动传递:
X-Trace-Id: A
如果项目使用:
RestTemplate
WebClient
OpenFeign
HttpClient
OkHttp
检查实际使用情况。
优先通过统一 Interceptor 实现。
例如:
Java
traceId=A
↓
HTTP Header
X-Trace-Id=A
↓
Python AI
traceId=A
这样 Java 和 Python 日志可以使用同一个 TraceId 检索。
十三、Netty / MQTT / TCP / UDP
这是 IoT 项目的重要场景。
检查项目现有:
Netty
MQTT
TCP
UDP
CoAP
入口。
对于每一个独立的消息 / 请求:
如果存在上游 TraceId:
继承
否则:
生成
然后进入:
TraceContext
例如:
设备 DEV001
↓
MQTT
↓
traceId=M001
↓
数据解析
↓
物模型
↓
数据库
↓
告警
整条处理链使用:
traceId=M001
注意:
设备每次独立上报消息属于独立业务事件,因此通常应该生成新的 TraceId,而不是让一个设备永久使用同一个 TraceId。
十五、SaaS 上下文
项目未来是 SaaS,因此 TraceContext 预留:
traceId
tenantId
userId
如果当前项目已经有:
tenantId
userId
则在请求认证成功后自动放入上下文。
例如:
traceId=A
tenantId=10001
userId=9527
但不要因为本次任务而大范围修改现有权限系统。
只需要提供统一上下文承载能力。
十六、线程安全
这是重点。
必须考虑:
Tomcat线程池复用
Quartz线程池
业务线程池
CompletableFuture
MQ Consumer线程
Netty EventLoop
任何任务结束后都必须:
clear
禁止出现:
请求A
traceId=A
请求结束
请求B
traceId=A
这种严重的数据串链问题。
十七、异常处理
异常情况下 TraceId 也必须保留。
例如:
traceId=A
ERROR
设备数据处理失败
完整异常堆栈必须能够通过:
traceId=A
检索出来。
Web 异常响应最好也返回:
X-Trace-Id: A
这样前端报错:
TraceId: A
我就可以直接去 Kibana / 日志文件搜索:
traceId=A
定位问题。
十八、日志检索目标
最终必须支持:
grep "traceId=01KABC" app.log
或者 Kibana:
traceId:"01KABC"
一次找到:
HTTP请求
Service
数据库
线程池
MQ Producer
MQ Consumer
AI
异常
十九、代码结构要求
根据当前项目实际结构,合理放置。
如果存在类似:
common
framework
core
starter
优先放到公共基础模块。
推荐结构:
xxx-core
└── trace
├── TraceContext
├── TraceIdGenerator
├── TraceContextSnapshot
├── TraceContextRunnable
└── TraceContextCallable
xxx-web-starter
└── trace
└── TraceIdFilter
xxx-log-starter
└── trace
└── MDC / Logback
xxx-schedule-starter
└── trace
└── Quartz Trace
xxx-mq-starter
└── trace
├── Producer Trace
└── Consumer Trace
具体目录必须根据当前项目实际结构调整,不要机械套用。
二十、不要过度设计
当前项目是单体项目。
第一阶段:
必须实现:
TraceId
MDC
Web
ThreadPool
Quartz
MQ
HTTP
暂时不要自行实现完整:
Span
Trace Graph
Jaeger
Zipkin
SkyWalking
OpenTelemetry Collector
但是代码设计要为未来 OpenTelemetry 留出扩展点。
也就是说:
现在:
TraceId + MDC
未来:
TraceId + SpanId + ParentSpanId + OpenTelemetry
不要把当前 TraceContext 设计成未来无法迁移的死结构。
二十一、需要先做代码分析
在开始修改代码之前:
- 扫描整个项目结构。
- 找到 Spring Boot 启动模块。
- 找到 Web 请求入口。
- 找到现有 Filter / Interceptor。
- 找到现有线程池。
- 找到 CompletableFuture。
- 找到 Quartz / @Scheduled。
- 找到 MQ Producer。
- 找到 MQ Consumer。
- 找到 RestTemplate / Feign / HTTP Client。
- 找到 Netty / MQTT / TCP / UDP 入口。
- 找到 Logback 配置。
- 找到现有 MDC / ThreadLocal / RequestContext 实现。
- 检查项目是否已经存在类似 TraceId / RequestId 机制。
如果已经存在类似能力:
优先重构和统一,不要重复造轮子。
二十二、实现原则
遵守:
低侵入
高复用
自动传播
自动清理
统一入口
统一出口
业务代码尽可能不需要修改。
理想效果:
原来的代码:
log.info("开始处理设备");
完全不用修改。
自动变成:
traceId=A 开始处理设备
二十三、测试要求
实现完成后必须增加测试。
至少测试:
Test 1:HTTP
请求进入
↓
生成 TraceId
↓
Controller
↓
Service
↓
日志
↓
Response Header
确认 TraceId 一致。
Test 2:HTTP携带 TraceId
X-Trace-Id=A
确认后端仍然使用:
A
Test 3:线程池
Web traceId=A
↓
ThreadPool
确认:
traceId=A
Test 4:MQ
Producer traceId=A
↓
MQ Header
↓
Consumer
确认 Consumer:
traceId=A
Test 5:MQ无 TraceId
确认 Consumer 自动生成。
Test 6:定时任务
确认:
TASK traceId=A
整个任务一致。
Test 7:线程池污染
执行:
请求A
请求B
请求C
确认:
A ≠ B ≠ C
并且不会出现线程复用导致 TraceId 串线。
Test 8:异常
异常发生后:
traceId
仍然存在于 ERROR 日志。
Test 9:并发
同时执行大量请求:
100+
1000+
确认 TraceId 不串线。
二十四、最终输出要求
完成后请给我:
1. 修改文件列表
例如:
新增:
xxx/TraceContext.java
修改:
xxx/Logback.xml
xxx/xxxFilter.java
2. 架构说明
说明:
Web
↓
TraceContext
↓
MDC
ThreadPool
↓
Context Propagation
MQ
↓
Message Header
HTTP
↓
X-Trace-Id
3. 使用示例
告诉我业务代码以后应该怎么写。
目标是:
log.info("开始处理");
而不是让业务代码手动管理 TraceId。
4. 测试结果
说明所有测试是否通过。
5. 风险
明确指出:
- 哪些场景已经支持
- 哪些场景暂未支持
- 是否存在 MDC 丢失风险
- 是否存在线程池上下文污染风险
- 是否存在 MQ TraceId 丢失风险
6. 后续演进建议
最后说明未来如何从:
TraceId + MDC
演进到:
TraceId
+
SpanId
+
ParentSpanId
+
OpenTelemetry
但本次不要提前实现完整分布式链路追踪系统。
最终验收标准
只有同时满足以下条件才算完成:
traceId=A
│
┌───────┴───────┐
│ │
Web Task
│
▼
Service
│
┌───────┴───────┐
▼ ▼
ThreadPool MQ
│ │
│ ▼
│ Consumer
│ │
└───────┬───────┘
▼
AI/HTTP
│
▼
DB
同一业务链可以通过:
traceId=A
在日志文件、IDEA Console、Kibana 中完整检索。
尤其必须保证:
跨线程、跨线程池、跨 MQ、跨 HTTP 的 TraceId 不丢失;任务执行完成后上下文必须清理,不允许线程复用造成 TraceId 污染。
请先分析当前代码,再实施修改。
不要在没有检查项目现有实现的情况下直接创建重复的 TraceId / MDC / ThreadLocal 体系。