业务链路追踪日志设计模式 --- 从原理到实践
一、什么是业务链路追踪日志
业务链路追踪日志是一种将复杂业务流程中的每个关键步骤以"节点"形式记录下来的技术方案。它不同于普通的 log 日志,核心区别在于:
| 对比项 | 普通日志(log.info) | 链路追踪日志 |
|---|---|---|
| 存储位置 | 文件/ELK | 独立数据库(MySQL) |
| 查询方式 | 按关键字搜索 | 按业务单号聚合,按节点排序展示 |
| 展示形式 | 纯文本 | 结构化(文本+表格+JSON+图标) |
| 使用场景 | 开发排查 | 业务人员+开发人员排查 |
| 关联性 | 散落各处 | 同一业务号串联所有节点 |
一句话理解:把一个业务流程拆成若干步骤,每个步骤执行完后发一条结构化消息到 MQ,消费端存入数据库,前端页面按业务单号查询并按步骤顺序展示。
二、整体架构
┌─────────────────────────────────────────────────────────┐
│ 业务服务(生产者) │
│ │
│ 业务方法() { │
│ 步骤1 → 发送节点1日志 │
│ 步骤2 → 发送节点2日志 │
│ 步骤3 → 发送节点3日志 │
│ ... │
│ return → 发送最终结果节点 │
│ } │
└────────────────────┬────────────────────────────────────┘
│ sendAsync(异步,不阻塞业务)
▼
┌─────────────────────────────────────────────────────────┐
│ RocketMQ │
└────────────────────┬────────────────────────────────────┘
│ 消费
▼
┌─────────────────────────────────────────────────────────┐
│ 链路追踪服务(消费者) │
│ │
│ 1. 反序列化消息 │
│ 2. 从"寻源入参"节点提取查询字段写入主表 │
│ 3. 将节点内容写入明细表 │
│ 4. 从"返回最终结果"节点提取耗时更新主表 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ MySQL │
│ │
│ 主表:sourcing_trace(业务号、状态、耗时、查询字段...) │
│ 明细表:sourcing_trace_node(节点名、顺序、内容JSON...) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 前端查询页面 │
│ │
│ 输入业务号 → 查主表 → 查明细表 → 按 nodeOrder 排序展示 │
│ 每个节点展示:节点名、状态标签、内容(文本/表格/JSON) │
└─────────────────────────────────────────────────────────┘
三、核心概念
3.1 业务号(sourcingBizNo)
同一次业务流程的所有节点共用一个业务号,用于聚合查询。类似于分布式追踪中的 traceId。
3.2 节点(Node)
业务流程中的一个关键步骤。每个节点有:
- nodeName:节点名称(如"入参校验"、"库存查询")
- nodeOrder:排序号(前端按此排序展示)
- status:SUCCESS / FAIL
- content:节点内容(结构化数据)
- nodeTag:标签文字(简短的结果摘要,如"通过"、"失败")
- nodeTagColor:标签背景色
3.3 内容项(ContentItem)
节点内容支持三种类型混合:
| 类型 | 用途 | 前端渲染 |
|---|---|---|
| TEXT | 键值对文本 | label: value(可带颜色) |
| TABLE | 表格数据 | 带表头的表格 |
| JSON | JSON 数据 | 代码块 + 复制按钮 |
3.4 标签(nodeTag + nodeTagColor)
标签是节点的"一眼结论",在前端列表中直接展示,不需要展开节点详情就能看到每个步骤的结果。
四、简单示例:订单下单流程
假设有一个下单接口,流程是:校验参数 → 检查库存 → 扣减库存 → 创建订单。
4.1 定义节点
| nodeOrder | nodeName | 说明 |
|---|---|---|
| 1 | 下单入参 | 记录原始请求 |
| 2 | 参数校验 | 校验商品、数量、地址 |
| 3 | 库存检查 | 检查库存是否充足 |
| 4 | 创建订单 | 生成订单号 |
| 99 | 最终结果 | 总耗时、成功/失败 |
4.2 示例代码
java
@Service
public class OrderService {
@Resource
private SourcingTraceProducer traceProducer;
public OrderResult createOrder(OrderRequest request) {
long startTime = System.currentTimeMillis();
String bizNo = generateBizNo(); // 生成业务号
try {
// ========== 节点1:下单入参 ==========
traceProducer.sendAsync(SourcingTraceMessage.builder()
.sourcingBizNo(bizNo)
.sourcingType("ORDER")
.systemSource("S001")
.scenarioName("普通下单")
.nodeName("下单入参").nodeOrder(1)
.nodeTime("2026-05-21 10:00:01")
.status("SUCCESS")
.nodeTag("通过")
.nodeTagColor("#E6FFEC")
.content(Arrays.asList(
ContentItem.text("商品编码", request.getProductCode()),
ContentItem.text("下单数量", String.valueOf(request.getQty())),
ContentItem.text("收货地址", request.getAddress()),
ContentItem.jsonObj("完整入参", request)
)).build());
// ========== 节点2:参数校验 ==========
validateParams(request); // 可能抛异常
traceProducer.sendAsync(SourcingTraceMessage.builder()
.sourcingBizNo(bizNo)
.sourcingType("ORDER").systemSource("S001")
.scenarioName("普通下单")
.nodeName("参数校验").nodeOrder(2)
.nodeTime("2026-05-21 10:00:01")
.status("SUCCESS")
.nodeTag("校验通过")
.nodeTagColor("#E6FFEC")
.content(Arrays.asList()).build());
// ========== 节点3:库存检查 ==========
int stockQty = checkStock(request.getProductCode());
boolean enough = stockQty >= request.getQty();
traceProducer.sendAsync(SourcingTraceMessage.builder()
.sourcingBizNo(bizNo)
.sourcingType("ORDER").systemSource("S001")
.scenarioName("普通下单")
.nodeName("库存检查").nodeOrder(3)
.nodeTime("2026-05-21 10:00:02")
.status(enough ? "SUCCESS" : "FAIL")
.nodeTag(enough ? "库存充足" : "库存不足")
.nodeTagColor(enough ? "#E6FFEC" : "#FFECE8")
.content(Arrays.asList(
ContentItem.text("当前库存", String.valueOf(stockQty)),
ContentItem.text("需求数量", String.valueOf(request.getQty()))
)).build());
if (!enough) {
throw new RuntimeException("库存不足");
}
// ========== 节点4:创建订单 ==========
String orderNo = doCreateOrder(request);
traceProducer.sendAsync(SourcingTraceMessage.builder()
.sourcingBizNo(bizNo)
.sourcingType("ORDER").systemSource("S001")
.scenarioName("普通下单")
.nodeName("创建订单").nodeOrder(4)
.nodeTime("2026-05-21 10:00:03")
.status("SUCCESS")
.nodeTag("创建成功")
.nodeTagColor("#E6FFEC")
.content(Arrays.asList(
ContentItem.text("订单号", orderNo)
)).build());
// ========== 节点99:最终结果 ==========
long costMs = System.currentTimeMillis() - startTime;
traceProducer.sendAsync(SourcingTraceMessage.builder()
.sourcingBizNo(bizNo)
.sourcingType("ORDER").systemSource("S001")
.scenarioName("普通下单")
.nodeName("最终结果").nodeOrder(99)
.nodeTime("2026-05-21 10:00:03")
.status("SUCCESS")
.nodeTag("下单成功")
.nodeTagColor("#E6FFEC")
.content(Arrays.asList(
ContentItem.text("总耗时", costMs + "ms"),
ContentItem.jsonObj("返回结果", result)
)).build());
return result;
} catch (Exception e) {
// ========== 节点99:最终结果(失败)==========
long costMs = System.currentTimeMillis() - startTime;
traceProducer.sendAsync(SourcingTraceMessage.builder()
.sourcingBizNo(bizNo)
.sourcingType("ORDER").systemSource("S001")
.scenarioName("普通下单")
.nodeName("最终结果").nodeOrder(99)
.nodeTime("2026-05-21 10:00:03")
.status("FAIL")
.nodeTag("下单失败")
.nodeTagColor("#FFECE8")
.content(Arrays.asList(
ContentItem.text("总耗时", costMs + "ms"),
ContentItem.text("失败原因", e.getMessage())
)).build());
throw e;
}
}
}
4.3 前端展示效果
查询业务号后,页面展示类似:
业务号:ORD260521100001
场景:普通下单
状态:成功
耗时:120ms
节点列表:
┌────┬──────────┬──────────┬─────────────────────────────┐
│ 序号 │ 节点名称 │ 标签 │ 内容摘要 │
├────┼──────────┼──────────┼─────────────────────────────┤
│ 1 │ 下单入参 │ [通过] │ 商品:ABC001, 数量:10 │
│ 2 │ 参数校验 │ [校验通过] │ │
│ 3 │ 库存检查 │ [库存充足] │ 当前库存:50, 需求:10 │
│ 4 │ 创建订单 │ [创建成功] │ 订单号:SO260521001 │
│ 99 │ 最终结果 │ [下单成功] │ 总耗时:120ms │
└────┴──────────┴──────────┴─────────────────────────────┘
五、设计要点
5.1 异步发送,不影响业务
java
// 用 sendAsync,MQ 发送失败只打日志,不影响业务主流程
traceProducer.sendAsync(message);
5.2 业务号贯穿全流程
同一次请求的所有节点必须使用同一个 bizNo,这是聚合查询的唯一依据。
5.3 节点可乱序到达
消息通过 MQ 异步发送,到达顺序不保证。前端按 nodeOrder 排序展示,不依赖消息到达顺序。
5.4 标签是"一眼结论"
标签(nodeTag)的作用是让用户不展开节点详情就能快速判断每个步骤的结果。设计原则:
- 简短(2-4个字)
- 有明确的成功/失败语义
- 颜色区分(绿色底=成功,红色底=失败)
5.5 特殊节点约定
服务端会从特定节点中提取字段:
- 第一个节点(入参节点):提取查询字段(如客户编码、商品编码等)写入主表,用于列表页搜索
- 最后一个节点(最终结果):提取"总耗时"写入主表
5.6 双层 try-catch 模式
java
try {
// 步骤1
try {
doStep1();
sendNode1(SUCCESS);
} catch (Exception e) {
sendNode1(FAIL);
throw e;
}
// 步骤2
try {
doStep2();
sendNode2(SUCCESS);
} catch (Exception e) {
sendNode2(FAIL);
throw e;
}
// 最终结果(成功)
sendFinalNode(SUCCESS);
return result;
} catch (Exception e) {
// 最终结果(失败)
sendFinalNode(FAIL);
throw e;
}
这种模式确保:
- 每个步骤失败时,对应节点记录 FAIL
- 无论哪个步骤失败,最终结果节点都会记录 FAIL
- 失败后的步骤不再发送节点日志
5.7 工具方法抽取
当节点多时,每次构建 Message 的重复代码很多。建议抽取工具方法:
java
private void sendTraceLog(String bizNo, String nodeName, int nodeOrder,
String status, List<ContentItem> content,
String nodeTag, String nodeTagColor) {
SourcingTraceMessage message = SourcingTraceMessage.builder()
.sourcingBizNo(bizNo)
.sourcingType("ORDER")
.systemSource("S001")
.scenarioName("普通下单")
.nodeName(nodeName)
.nodeOrder(nodeOrder)
.nodeTime(LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
.status(status)
.nodeTag(nodeTag)
.nodeTagColor(nodeTagColor)
.content(content)
.build();
traceProducer.sendAsync(message);
}
六、常量管理
将重复使用的字符串抽取为常量,避免硬编码:
java
public final class TraceConstants {
// 状态
public static final String STATUS_SUCCESS = "SUCCESS";
public static final String STATUS_FAIL = "FAIL";
// 内容文字颜色
public static final String COLOR_SUCCESS = "#52c41a"; // 绿色文字
public static final String COLOR_FAIL = "#ff4d4f"; // 红色文字
// 标签背景色
public static final String TAG_COLOR_SUCCESS = "#E6FFEC"; // 绿色底
public static final String TAG_COLOR_FAIL = "#FFECE8"; // 红色底
}
注意区分:
COLOR_SUCCESS/FAIL:用于 ContentItem 中文字的颜色(深绿/深红)TAG_COLOR_SUCCESS/FAIL:用于标签的背景色(浅绿/浅红)
七、适用场景
这种设计模式适合以下场景:
- 业务流程步骤多,排查问题时需要知道"卡在哪一步"
- 需要业务人员(非开发)也能看懂流程执行情况
- 需要按业务单号快速定位问题
- 流程涉及多个外部系统调用,需要记录每次调用的入参和出参
不适合的场景:
- 简单的 CRUD 操作
- 对性能极度敏感的高频接口(虽然异步发送影响很小)
- 不需要业务人员参与排查的纯技术问题
八、总结
| 概念 | 类比 | 作用 |
|---|---|---|
| 业务号 | 快递单号 | 串联整个流程 |
| 节点 | 快递物流节点 | 记录每个关键步骤 |
| nodeOrder | 时间线顺序 | 前端排序展示 |
| nodeTag | 物流状态标签 | 一眼看到结果 |
| content | 物流详情 | 展开查看具体信息 |
| sendAsync | 异步通知 | 不阻塞主流程 |
整个设计的核心思想:用结构化的方式记录业务流程的每个关键步骤,让任何人都能通过一个业务号快速了解"发生了什么、在哪一步、为什么失败"。