日志格式设计:结构化 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 级日志的系统里,这个差距就很可观了。
字段设计原则:少即是多
日志字段不是越多越好。我见过有人把整个 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而不是requestIduser_agent而不是userAgentduration_ms而不是durationInMilliseconds
为什么选 snake_case?因为它在 Logfmt 里不需要转义,在 JSON 里也完全兼容。而且大多数日志分析工具(如 Elasticsearch、ClickHouse)对下划线命名字段支持得最好。
日志级别:别滥用 ERROR
说到日志级别,我见过最离谱的事——有人把所有日志都打成了 ERROR。问他为什么,他说「这样不会被过滤掉」。结果呢?告警系统天天报警,运维团队直接躺平了。
正确的做法是:
- FATAL:系统无法继续运行,比如数据库连接失败
- ERROR:需要立即处理的异常,比如支付失败
- WARN:不影响运行但值得关注,比如重试了 3 次才成功
- INFO:关键业务流程的节点,比如订单创建成功
- DEBUG:开发调试用,生产环境建议关闭
小结
日志格式设计这件事,说白了就是为未来的自己铺路。你现在花 10 分钟设计好格式,将来排查问题时就能省下 10 个小时。结构化日志、统一的字段命名、合理的日志级别——这三样东西,是我做日志系统时雷打不动的铁律。
下一章我会聊聊日志采集架构,包括怎么用 Filebeat 和 Fluentd 把日志从各个服务里捞出来。到时候见。