全链路日志系统

用户只看到「下单失败」。背后可能穿过网关、订单、库存、支付和消息队列。若每个系统各写各的日记本,工程师就只能拿时间戳人肉拼图——全链路日志,就是给这次请求发一张全球通行证。

一句话:统一 TraceId、自动透传、结构化可检索;让故障从「搜日志」变成「还原一次请求」。

从“搜日志”到“还原一次请求”

普通日志回答“这个服务发生了什么”;全链路日志还要回答:

  • 这条日志属于哪次用户请求?
  • 请求经过了哪些服务和步骤?
  • 时间耗在哪里,错误从哪里开始传播?
  • 异步消息、重试和批处理如何关联回来?

三个概念最关键:

字段 含义
TraceId 一条端到端链路的全局标识
SpanId 链路中一次操作的标识,如一次 RPC 或 SQL
ParentSpanId 当前 Span 的父操作,用于还原调用树

一次请求可看成一棵树:

1
2
3
4
5
Trace 7f3a...
└─ gateway span=A
└─ order-service span=B parent=A
├─ mysql span=C parent=B
└─ inventory-rpc span=D parent=B

所有节点共享 TraceId,但 SpanId 不同。只记录 TraceId 能把日志搜齐;加入 Span 父子关系,才能看清调用结构与耗时。

一套典型架构

全链路可观测通常包含三类信号:

  • Logs:离散事件和上下文,适合解释“为什么”;
  • Metrics:聚合数字和趋势,适合发现“是否异常”;
  • Traces:请求路径与耗时,适合定位“慢在哪里”。

典型数据流如下:

1
2
3
4
5
6
应用 SDK / Agent
├─ stdout 日志 ─→ Fluent Bit / Vector ─→ Kafka ─→ Elasticsearch/Loki
├─ Trace Span ─→ OpenTelemetry Collector ─→ Tempo/Jaeger/APM
└─ Metrics ─→ Collector/Exporter ─→ Prometheus
│
Grafana / 告警平台

采集器应与应用解耦。应用只负责规范输出,Agent 或 Collector 负责批量、缓冲、重试和路由,后端负责索引、存储与查询。

OpenTelemetry 提供厂商中立的 API、SDK 和传输规范。实践中可采用 W3C Trace Context,用 HTTP 头传播:

1
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

字段依次包含版本、Trace ID、父 Span ID 和采样标志。不要自创十几套 X-Request-Id 协议;若历史系统已有 request ID,可以保留并映射,但链路传播尽量统一。

上下文如何跨边界传播

同步 RPC 最简单:调用方把 Trace Context 注入 HTTP/gRPC 头,被调用方提取后创建子 Span,响应返回时结束 Span。

异步场景更容易断链:

  1. 生产者创建“发送消息”Span;
  2. 把 trace 上下文写入消息 Header,而非业务正文;
  3. 消费者提取上下文,创建“处理消息”Span;
  4. 重试时保留原始关联,同时记录 retry count;
  5. 批量消费可用 Span Link 关联多个上游,而不是硬造一个父节点。

线程池、协程与定时任务也要注意上下文丢失。不要假设 ThreadLocal 能自动跨线程;应使用框架的 context propagation 能力,或显式传递不可变上下文。

链路入口若没有上游 TraceId,就生成新的;收到格式非法或不可信的外部 TraceId 时,应校验并限制长度,避免日志注入与索引污染。

结构化日志:让机器能理解

下面两行对人都能看懂,但只有后一种适合稳定查询:

1
2
下单失败,用户 42,商品 9001,库存不足
{"level":"WARN","event":"order_create_failed","user_id":"42","sku_id":"9001","reason":"out_of_stock"}

推荐的基础字段:

类别 字段示例
时间 timestamp,统一 UTC 或明确时区
级别 level:DEBUG/INFO/WARN/ERROR
服务身份 service.name、service.version、environment
链路 trace_id、span_id
事件 event、message、error.type、error.stack
请求 http.method、http.route、status_code、duration_ms
业务 order_id、tenant_id 等稳定标识

message 用于人读,字段用于机器查。不要把所有信息都塞进一句可变文本,更不要把动态订单号放进字段名。

一个较完整的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"timestamp": "2026-09-29T02:30:18.234Z",
"level": "ERROR",
"service.name": "order-service",
"service.version": "v2.7.1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"event": "payment_call_failed",
"order_id": "o_83921",
"duration_ms": 812,
"error.type": "UpstreamTimeout",
"message": "payment gateway timed out"
}

日志 Schema 应版本化并有负责人。字段命名可优先参考 OpenTelemetry Semantic Conventions,减少不同团队的 cost、latency、elapsed 三套同义词。

日志级别与错误记录

级别不是情绪,而是运维契约:

  • DEBUG:调试细节,生产通常按需开启;
  • INFO:关键业务状态变化与生命周期事件;
  • WARN:已降级或可恢复,但值得关注;
  • ERROR:本次操作失败,需要调查;
  • FATAL:进程无法继续。

同一异常不要每层都打印完整堆栈。底层可以包装错误并返回,上层在真正处理或终止请求的边界记录一次,否则一个故障会制造几十条重复 ERROR,污染告警。

日志还要避免两种极端:入口、出口、每个变量都打,成本失控;只在 catch 里写“失败了”,没有参数、耗时和错误类型,无法定位。

采样:省成本但别丢掉真相

流量很大时,保存每个 Trace 和每条 DEBUG 日志并不现实。

头部采样

请求刚进入系统就决定是否采样,例如保留 1%。优点是开销低、整条链路决策一致;缺点是无法提前知道这次请求最终会不会报错。

尾部采样

Collector 暂存完整 Trace,结束后再决定:

  • 错误请求 100% 保留;
  • 高延迟请求提高比例;
  • 普通成功请求只保留少量;
  • 特定租户或灰度版本按规则保留。

尾部采样更聪明,但需要内存、等待窗口和集中式决策。无论哪种,都要传播采样标志,避免上游采了、下游没采。

日志采样不能简单随机扔掉 ERROR。对重复错误可按指纹限速,同时记录被抑制数量;审计日志与计费记录更不能因采样消失。

隐私与安全

日志系统往往比业务库更容易被广泛查询,因此也是数据泄露高风险区。默认禁止记录:

  • 密码、Token、Cookie、私钥;
  • 完整身份证、银行卡、手机号;
  • 请求或响应的完整敏感正文;
  • 未经批准的用户内容。

推荐在源头做字段白名单和脱敏,而不是指望采集端用正则补救。哈希也不是万能匿名化:手机号空间有限,普通 SHA-256 很容易被字典反查;需要关联统计时应使用带密钥的 HMAC,并管理密钥轮换。

日志查询要有最小权限、审计记录、保留周期和自动删除策略。开发环境与生产环境应分开索引,导出日志同样要审批和脱敏。

性能与可靠性

日志代码不能反过来拖垮业务:

  • 异步批量写,设置队列上限;
  • 后端不可用时限速或落盘缓冲;
  • 明确“队列满时丢低级日志还是阻塞业务”;
  • 限制单条大小与异常堆栈长度;
  • 避免对每个高基数字段建立索引。

user_id、trace_id 很有查询价值,但 URL 原文、SQL 原文、随机错误消息可能造成索引基数爆炸。可把 http.route=/users/{id} 作为聚合字段,把原始路径仅作为非索引字段或干脆不记。

系统自身也要可观测:采集延迟、丢弃条数、队列长度、解析失败率、存储容量、查询 P95 都应有指标与告警。

实施清单

第一阶段:先做到“串得起来”

  1. 统一入口生成或接收 W3C Trace Context;
  2. HTTP、RPC、MQ 自动注入与提取;
  3. 所有结构化日志自动带 trace_id/span_id;
  4. 网关到核心服务跑通一条可查询链路。

第二阶段:让数据可用

  1. 定义日志 Schema、级别与脱敏规范;
  2. 接入 OpenTelemetry Collector;
  3. 建立按服务、版本、路由和错误类型的面板;
  4. 告警可一键跳转到对应 Trace 和日志;
  5. 用故障演练验证跨线程、MQ、重试不会断链。

第三阶段:治理成本

  1. 按价值设置采样策略;
  2. 管理索引与冷热保留周期;
  3. 监控采集丢失和 Schema 解析失败;
  4. 定期扫描敏感字段与无效日志;
  5. 用每 GB 成本、检索命中率和故障定位时间衡量收益。

小结

全链路日志不是“给每行加一个 TraceId”就结束了。它是一整套上下文传播、结构化 Schema、采集管道、采样策略和安全治理。

成熟系统的体验应该是:从一个告警进入,先看指标确认范围,再打开 Trace 找到最慢或出错的 Span,最后跳到同一 TraceId 的结构化日志解释原因。三类信号互相跳转,才真正把“日志海洋”变成故障证据链。