用户提交导出任务,API 返回 202,几分钟后页面只显示“导出失败”。前端有 requestId,API 日志能查到入队成功,Worker 日志却使用另一个 ID。我们只能按时间、用户和文件名猜哪条任务对应哪次请求,跨过消息队列后链路就断了。
后来接入 OpenTelemetry 时,我们没有只给 HTTP 自动埋点,而是明确规定上下文怎样进入消息头、怎样在 Worker 恢复,以及异步重试如何表达新的执行尝试。
图 1:队列不是追踪终点;生产 span 与消费 span 通过消息上下文关联。
traceId 与 jobId 解决不同问题
traceId 描述一次执行链,适合性能和错误定位;jobId 描述长期业务任务,跨重试、暂停和人工恢复保持稳定。一次 Job 可能对应多个 trace。
type ExportMessage = {
jobId: string;
attempt: number;
tenantId: string;
payload: ExportInput;
};trace context 放消息 headers,不混入业务 payload。日志同时记录 traceId、spanId、jobId 和 attempt,既能沿执行链看时间,也能汇总任务历史。
生产消息时注入上下文
import { context, propagation, trace } from "@opentelemetry/api";
const headers: Record<string, string> = {};
propagation.inject(context.active(), headers);
await queue.publish("export.requested", message, { headers });消息中间件可能只接受字符串 header,需要显式序列化。不要把整个上下文对象 JSON 化,因为标准传播字段和采样标志需要被其他语言理解。
消费时恢复,但不伪造同步父子关系
队列消息可能等待很久、被多个消费者处理或批量消费。我们根据语义选择 parent 或 link。一次普通单消费可以以提取上下文为父:
const parent = propagation.extract(context.active(), delivery.headers);
await context.with(parent, async () => {
const tracer = trace.getTracer("export-worker");
return tracer.startActiveSpan("export.process", async span => {
try {
await processExport(delivery.message);
span.setStatus({ code: 1 });
} catch (error) {
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
});
});批处理由多个消息共同触发时,更适合新建 span 并链接多个来源,避免假装只有一个父节点。
采样不能让错误证据全部消失
全量 trace 成本太高,我们对普通成功请求低比例采样,对高风险任务和错误提高保留率。头部采样在请求开始时决定,无法预知后续失败;因此 Collector 还使用尾部采样,根据错误、长延迟和关键属性决定保留整条 trace。
| 属性 | 用途 | 注意 |
|---|---|---|
service.name |
区分服务 | 使用稳定名称,不含实例 ID |
job.type |
分析任务类型 | 控制基数 |
tenant.id |
权限过滤与聚合 | 按政策脱敏,不作为公开标签 |
attempt |
区分重试 | 与 jobId 配合 |
error.code |
聚合失败类型 | 不使用动态 message |
上下文传播也要做契约测试
我们在集成环境发送带已知 traceparent 的消息,断言 Worker span 与其关联;重试产生新 span 但 jobId 不变;死信记录最后 traceId;日志能够从 trace 跳到 job 页面,再从 job 页面列出所有 attempts。
链路接通后,那类“API 成功、后台失败”的问题不再靠时间猜测。更重要的是,我们没有把 traceId 当万能业务 ID。追踪关注一次执行,业务模型关注一个长期任务;把两者同时保留,系统才既能诊断性能,也能解释用户结果。
这两类 ID 的边界会在 Agent 场景里被进一步放大:模型的一次调用既是执行链的一部分,又是一个需要审批、重试和补偿的长期任务。我在人工审批不是弹窗里把 planId、taskId 与执行尝试分离开,正是同一原则在事务边界的延伸。可观测性的整体组织方式则在可观测性不是一块 Grafana 大屏里展开。