日志格式设计:结构化 vs 非结构化,以及那些你不得不懂的字段设计

聊到日志格式,我见过太多团队在这个问题上栽跟头了。说实话,日志格式选对了,后面排查问题能省一半的力气。选错了?嗯,你会在海量文本里翻来翻去,最后发现关键信息被截断了——这种体验,我经历过不止一次。

非结构化日志:为什么我劝你尽早放弃

非结构化日志,说白了就是一段自由文本。比如:

2024-01-15 10:30:45 ERROR user login failed: invalid password, ip: 192.168.1.1

这种格式看起来挺直观的,对吧?但你想过没有,当你要从几百万条这样的日志里找出某个用户的登录失败记录时,怎么办?

我在一个老项目里接手过这种日志。当时要统计某个接口的 P99 延迟,我写了整整三页的正则表达式。更崩溃的是,开发人员手一抖,把时间格式从 2024-01-15 改成了 2024/01/15,所有正则全部报废。

避坑指南: 我曾经因为非结构化日志的字段顺序变更,导致整个告警系统误报了一整夜。从那以后,我定了个规矩:生产环境不允许出现非结构化日志。

结构化日志:这才是现代系统的标配

结构化日志,就是把日志变成机器可解析的键值对。你想想看,既然数据库都用结构化数据,为什么日志要例外?

目前主流的结构化格式有两种:JSON 和 Logfmt。

JSON 格式

{
  "timestamp": "2024-01-15T10:30:45.123Z",
  "level": "ERROR",
  "logger": "auth_service",
  "message": "user login failed",
  "user_id": "u_10086",
  "reason": "invalid_password",
  "ip": "192.168.1.1",
  "request_id": "req_abc123",
  "duration_ms": 45
}

JSON 的好处是生态好,几乎所有日志收集系统都原生支持。但有个坑——JSON 解析开销大,在高并发场景下,序列化本身就会吃掉不少 CPU。

Logfmt 格式

timestamp=2024-01-15T10:30:45.123Z level=ERROR logger=auth_service msg="user login failed" user_id=u_10086 reason=invalid_password ip=192.168.1.1 request_id=req_abc123 duration_ms=45

我个人更偏爱 Logfmt。为什么?因为它比 JSON 更紧凑,解析速度也更快。你看上面两段日志,同样的信息,Logfmt 少了将近一半的字符。在每天产生 TB 级日志的系统里,这个差距就很可观了。

我的建议: 如果团队刚接触结构化日志,先用 JSON,生态支持最全。等你们对性能有更高要求了,再考虑切换到 Logfmt。我在一个日处理 10 亿条日志的系统里做过对比,Logfmt 的解析速度比 JSON 快了约 30%。

字段设计原则:少即是多

日志字段不是越多越好。我见过有人把整个 HTTP 请求体都塞进日志里,结果 Elasticsearch 集群直接撑爆了。字段设计要遵循几个原则:

原则 说明 我的经验
必选字段 timestamp, level, logger, message, request_id 没有 request_id 的日志,排查问题时基本等于废的
业务字段 user_id, order_id, 接口名等 只加那些你确实会用到的,别为了「万一」加字段
性能字段 duration_ms, cpu_usage, memory_usage 建议采样记录,全量记录成本太高
上下文字段 ip, user_agent, trace_id trace_id 一定要有,分布式追踪全靠它

一个标准的日志格式模板

下面是我在多个项目中沉淀下来的模板,你可以直接拿来用:

{
  "timestamp": "2024-01-15T10:30:45.123Z",
  "level": "INFO",
  "logger": "payment_service",
  "message": "order created successfully",
  "request_id": "req_abc123",
  "trace_id": "trace_xyz789",
  "user_id": "u_10086",
  "order_id": "ord_20240115_001",
  "duration_ms": 123,
  "extra": {
    "payment_method": "wechat",
    "amount": 99.00
  }
}

注意看 extra 字段。我习惯把那些不常查询的、或者格式不固定的信息放到一个嵌套对象里。这样既保留了灵活性,又不会污染主字段的索引结构。

核心要点: 日志格式设计的本质,是在「可读性」和「可解析性」之间找平衡。结构化日志牺牲了一点可读性,换来了机器解析的便利。而这个取舍,在微服务架构下是值得的。

字段命名规范:统一就是效率

字段命名这件事,看似小事,但我在跨团队协作时吃过不少亏。比如有的团队用 user_id,有的用 userId,还有的用 uid。结果就是,写查询语句时得记住三套命名规则。

我建议统一使用 snake_case(下划线命名法):

  • request_id 而不是 requestId
  • user_agent 而不是 userAgent
  • duration_ms 而不是 durationInMilliseconds

为什么选 snake_case?因为它在 Logfmt 里不需要转义,在 JSON 里也完全兼容。而且大多数日志分析工具(如 Elasticsearch、ClickHouse)对下划线命名字段支持得最好。

日志级别:别滥用 ERROR

说到日志级别,我见过最离谱的事——有人把所有日志都打成了 ERROR。问他为什么,他说「这样不会被过滤掉」。结果呢?告警系统天天报警,运维团队直接躺平了。

正确的做法是:

  • FATAL:系统无法继续运行,比如数据库连接失败
  • ERROR:需要立即处理的异常,比如支付失败
  • WARN:不影响运行但值得关注,比如重试了 3 次才成功
  • INFO:关键业务流程的节点,比如订单创建成功
  • DEBUG:开发调试用,生产环境建议关闭
注意: 我曾经在一个项目里发现,DEBUG 级别的日志占了总日志量的 80%,而真正有用的 ERROR 日志反而被淹没了。后来我们强制要求:生产环境只允许 INFO 及以上级别,DEBUG 日志必须通过动态配置开启。

小结

日志格式设计这件事,说白了就是为未来的自己铺路。你现在花 10 分钟设计好格式,将来排查问题时就能省下 10 个小时。结构化日志、统一的字段命名、合理的日志级别——这三样东西,是我做日志系统时雷打不动的铁律。

下一章我会聊聊日志采集架构,包括怎么用 Filebeat 和 Fluentd 把日志从各个服务里捞出来。到时候见。