2301 字
约 7 分钟
22
S16-A27 · 日志字段的约定:谁定义、怎么演进
2024-12-31
2026-11-09

一条告警指向"某接口错误率上升",运维打开日志平台,发现这个服务的日志里没有 trace_id,只有 requestId;另一个服务里有 trace_id 但拼成了 traceId。两个服务都要查,于是要写两套查询语句,而且无法把两个服务的事件串起来。

日志字段看起来是"日志格式问题",实际是跨团队的查询契约。字段名不一致的代价不在写日志时,而在最需要它的时候——排障期间、时间紧、还要先花十分钟搞清字段叫什么。


1. 字段分三层,演进规则不同

层 例子 谁定义 演进约束
基础字段 ts、level、service、msg 平台 不可变
关联字段 trace_id、span_id、user_id 平台(强制) 不可变
业务字段 order_id、amount、channel 业务团队 可增不可改

前两层必须统一,因为它们承担"把事件串起来"的职责。第三层应当允许自定义,否则业务会被迫把信息塞进 msg 字符串里——那才是真正的不可查询。

字段还能按"是否可枚举"再分一次,它决定该用索引还是该用全文检索:

字段形态 例子 该有的查询方式
低基数枚举 level、env、status 索引过滤 + 聚合
高基数标识 order_id、trace_id 精确匹配
自由文本 msg、error.stack 全文检索
数值 duration_ms、amount 范围过滤

把高基数标识放进全文检索、把自由文本当成索引字段,都会让查询成本失控。

关键区分:基础与关联字段不允许各团队自定,业务字段必须有明确归属。 混淆这两者,要么失去关联能力,要么失去扩展能力。

2. 关联字段为什么必须强制

字段 作用 缺失后果
trace_id 跨服务串一条请求链 只能按时间猜
span_id 定位链中某一跳 无法区分同链多次调用
user_id 按人定位影响范围 无法回答"影响多少人"
request_id 单服务内部幂等与去重 重试无法区分

第三项在事故沟通中最有用:"影响多少用户"通常是第一个被问到的问题,而它只能靠 user_id 回答。

第四项易被与 trace_id 混为一谈:前者标识"一次调用尝试",后者标识"一次业务请求"。重试时 trace_id 相同而 request_id 不同,这个差别是排查重复处理的关键。

3. 字段名的约定怎么落地

手段 强制性 成本
结构化日志库 + 强制字段 高 中(统一依赖)
采集侧补字段(如从 header 提取) 高 低(平台改)
规范文档 + 评审 低 低但不可靠
日志质量检查(管道中校验) 中高 中

第二种最划算:如果 trace_id 来自请求头,采集侧可以统一补齐,业务代码不需要关心。这样既保证了字段存在,又不强迫每个团队改代码。

// 采集侧统一补齐:业务只写业务字段
processors:
  add_fields:
    fields:
      service.name: "${SERVICE_NAME}"
      env: "${ENV}"
  extract_from_header:
    source: http.request.header
    rules:
      trace_id: "x-trace-id"
      user_id: "x-user-id"
  // 缺失时打标而不是丢弃:让"没传"变成可统计的事实
  on_missing: mark_missing

采集侧补齐还有个边界要注意:不能补出错误的值。如果请求头里没有 trace_id,绝不能生成一个随机的——那会让不同请求看起来像同一条链。正确做法就是打标为缺失。

on_missing: mark_missing 这一项很重要:如果缺失就直接丢弃,你永远不会知道有多少请求没传;打上 trace_id=missing 反而能统计出缺口比例。

4. 业务字段的演进规则

变更 允许 说明
新增字段 是 下游忽略未知字段即可
重命名字段 否(需过渡期) 先双写两个名字,再删旧的
删除字段 否(需过渡期) 先停写,观察一个周期
改变语义 否 等于新增一个字段
改变格式 视情况 时间格式、精度都要过渡

第四项最容易被忽略:把 duration_ms 的含义从"业务处理耗时"改成"含网络往返的耗时",字段名没变,所有历史看板的口径都变了,而且不会报错。

业务字段的另一个常见问题是枚举值不统一:同一个状态在三个服务里分别记成 PAID、paid、2。跨服务聚合时这三者互不相认,而每种都有自己的"正确性"。

// 语义变更的正确做法:加新字段,双写一段时间
旧:duration_ms = 业务处理耗时
新:duration_ms = 业务处理耗时(保持)
    duration_total_ms = 含网络往返(新增)
// 看板逐步切到新字段,旧字段保留一个季度后下线

枚举统一不必一次到位,可以从"新增字段用统一值 + 旧字段保留"开始:

// 枚举统一的过渡写法
旧:status = "PAID" | "paid" | "2"
新:pay_status = "paid"(新字段,统一小写)
    pay_status_code = 2(如确需数字,单独字段)
// 看板切到新字段;旧字段在兼容期后停止写入

5. 字段的可查询性

问题 后果
高基数文本字段(如 URL 全路径) 索引膨胀,查询变慢
把 JSON 塞进 msg 无法按字段过滤
字段类型不一致(有时数字有时字符串) 查询与聚合失效
缺单位 数字无意义(ms?s?)

第四项成本最低、收益最高:所有时长字段带单位后缀(_ms、_bytes),所有金额字段带币种或统一单位。没有单位的数字在跨服务对比时会引发争议。

// 一个可查询的结构化日志样例
{ "ts":"2026-11-10T05:12:33.412Z", "level":"warn", "service":"order",
  "trace_id":"9f2c...", "request_id":"req-8812", "user_id":10042,
  "event":"payment.retry", "order_id":"O-7781",
  "duration_ms":842, "attempt":2, "reason":"timeout" }
// 关键:业务字段扁平、带单位、类型稳定;不把结构化信息塞进 msg

6. 排错:五种"日志查不到"

现象 成因 处理方向
无法跨服务串联 trace_id 缺失或名字不同 采集侧统一补齐
按用户查不到 user_id 未记或只在入口记 强制字段,贯穿链路
聚合结果为零 字段类型不一致 统一类型,采集侧转换
时间范围对不上 时区不一致或无时区 统一 UTC + ISO8601
关键字段被截断 字段长度上限 调整映射或改存摘要

排错时还有一个入口值得先试:用一条已知存在的记录反查字段名。从一条日志里复制出全部字段名,比读文档更快,也更接近实际。

// 从一条真实日志里导出字段名清单,作为约定的核对基准
curl -s "$LOG_API/search?q=trace_id:$TRACE&size=1" | jq -r '.hits[0]._source | keys[]' | sort

第五项很实际:字符串字段在多数日志平台有长度上限,超长会被截断,于是"最长的那些异常信息"恰好被切掉。重要的不是记完整堆栈,而是记可定位的摘要 + 引用。

7. 边界:什么情况不必统一字段

场景 为什么 做法
单服务、无跨服务调用 无需串联 只需基础字段
调试日志(非结构化) 只给人看 不强制结构
临时脚本输出 生命周期短 自由格式
已有独立的追踪系统 关联由追踪承担 日志不必重复

判断口径一句话:这个字段会不会被另一个团队用来查询。会,它就必须进约定;不会,它只是本团队的习惯。


8. 自测

  1. 日志字段三层中,哪一层允许自定义、哪一层不允许?混淆两者会分别失去什么?
  2. trace_id 与 request_id 的差别是什么?重试场景下为什么必须区分?
  3. 为什么"采集侧补字段"比"规范文档 + 评审"更可靠?
  4. 缺失字段为什么应当打标而不是丢弃?打标后能得到什么数字?
  5. 改变字段语义为什么比改名更危险?正确的过渡做法是什么?
  6. 时长字段带单位后缀的成本与收益分别是什么?
S16-A27 · 日志字段的约定:谁定义、怎么演进
https://www.clxhxhhr.top/posts/6416/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。