3. 日志格式设计:结构化日志 vs 非结构化日志、日志字段规范、日志级别设计
日志格式这事儿,看着简单,其实坑特别多。我见过太多团队,一开始随便写写,等到系统出问题了,几万行日志翻下来,眼睛都快瞎了,还是找不到根因。今天咱们就把日志格式这件事彻底聊透。
3.1 非结构化日志:为什么我劝你尽早放弃
非结构化日志,说白了就是「想怎么写就怎么写」。比如这样:
2024-01-15 10:23:45 ERROR 用户下单失败,订单号: ORD20240115102345,原因是库存不足
2024-01-15 10:23:46 INFO 订单 ORD20240115102346 支付成功,金额 299.00 元
2024-01-15 10:23:47 WARN 连接数据库超时,重试第1次
看着挺清楚对吧?但问题来了——你想想看,如果我要统计今天所有支付成功的订单金额总和,你怎么搞?用正则表达式去匹配?嗯,我当年就是这么干的,结果写出来的正则又长又脆弱,稍微改个文案就全崩了。
非结构化日志的致命伤:
- 解析困难:每行日志的格式都不一样,机器没法自动理解
- 查询低效:想按订单号搜索?只能全文扫描
- 扩展性差:加个字段就得改所有下游解析逻辑
- 容易出错:开发人员手一抖,格式就歪了
⚠️ 我曾经踩过的坑:有一次线上排查问题,发现某条日志里订单号写成了「ORD20240115 102345」,中间多了个空格。就这一个空格,导致我们的告警系统漏掉了整整两小时的异常数据。从那以后,我对非结构化日志就彻底死心了。
3.2 结构化日志:这才是生产级的做法
结构化日志,说白了就是让日志变成机器能读懂的数据。最常见的格式是 JSON:
{
"timestamp": "2024-01-15T10:23:45.123Z",
"level": "ERROR",
"logger": "com.example.OrderService",
"thread": "http-nio-8080-exec-3",
"message": "用户下单失败",
"fields": {
"orderId": "ORD20240115102345",
"userId": "U12345",
"reason": "库存不足",
"productId": "P67890",
"stockRemaining": 0
}
}
这样做的好处太明显了:
- 机器友好:Elasticsearch、ClickHouse 这些工具直接就能索引
- 查询灵活:想查某个用户的所有订单?
fields.userId: "U12345"一秒搞定 - 自动聚合:统计错误类型分布、计算响应时间百分位,都是现成的
- 前后兼容:加字段不影响老数据的解析
💡 我的习惯:我个人习惯在日志里加一个
traceId 字段,用来串联一次请求经过的所有服务。这样排查问题时,拿着 traceId 一搜,整个调用链路就全出来了。
3.3 日志字段规范:该有的一个不能少
结构化日志不是字段越多越好。我见过有人一条日志塞了五十多个字段,结果 90% 都是冗余的。这里我总结了一套「黄金字段」:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
| timestamp | ISO 8601 | 精确到毫秒,带时区 | 2024-01-15T10:23:45.123Z |
| level | string | 日志级别,全大写 | ERROR, WARN, INFO, DEBUG |
| logger | string | 产生日志的类或模块名 | com.example.OrderService |
| thread | string | 线程名,方便定位并发问题 | http-nio-8080-exec-3 |
| traceId | string | 分布式追踪 ID | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
| message | string | 人类可读的描述 | 用户下单失败 |
| fields | object | 业务相关的键值对 | {"orderId": "ORD123", "userId": "U456"} |
| exception | object | 异常信息(仅 ERROR 级别) | {"type": "StockException", "stackTrace": "..."} |
🔑 核心原则:
1. 所有字段名统一使用驼峰命名(camelCase)
2. 时间戳统一使用 UTC 时间,避免时区混乱
3. 敏感信息(密码、身份证号等)绝对不能出现在日志里
4. 字段值尽量使用标准格式,比如金额用分而不是元
1. 所有字段名统一使用驼峰命名(camelCase)
2. 时间戳统一使用 UTC 时间,避免时区混乱
3. 敏感信息(密码、身份证号等)绝对不能出现在日志里
4. 字段值尽量使用标准格式,比如金额用分而不是元
3.4 日志级别设计:别把 INFO 当垃圾桶
日志级别这事儿,看着简单,但实际用起来特别容易走偏。我见过最夸张的案例,一个系统里 80% 的日志都是 INFO 级别,结果真正出问题的时候,ERROR 日志被 INFO 淹没了。
我的建议是这样的:
- FATAL:系统级灾难,比如数据库连不上、磁盘写满了。这种日志一出现,值班同学就该被电话叫醒了。
- ERROR:业务异常,比如下单失败、支付超时。需要人工介入排查,但不至于让系统挂掉。
- WARN:值得关注但不紧急的情况,比如重试、降级、限流触发。我习惯给 WARN 也配上告警,只是阈值设得宽松一些。
- INFO:关键业务流程的节点,比如「用户注册成功」「订单支付完成」。注意,不是所有操作都值得打 INFO,否则日志量会爆炸。
- DEBUG:开发调试用,线上环境默认关闭。我曾经犯过一个错,把 DEBUG 日志开到了生产环境,结果一天产生了 200GB 日志,直接把磁盘撑爆了。
- TRACE:比 DEBUG 更细的粒度,一般只在定位极端问题时临时开启。
⚠️ 避坑指南:我曾经接手过一个项目,代码里到处都是
logger.info("进入方法xxx") 和 logger.info("离开方法xxx")。这种日志除了刷屏,一点用都没有。记住,INFO 级别的日志应该是「有业务含义的事件」,而不是「代码执行轨迹」。
3.5 一张图看懂日志格式设计
下面这张图,是我自己总结的日志格式设计核心逻辑。说白了就是三个问题:记什么、怎么记、记多细。
3.6 实战建议:从零开始搭建日志规范
说了这么多理论,最后给点实在的。如果你现在要从零开始设计日志规范,我建议按这个步骤来:
- 先定级别:和团队一起定好每个级别的使用场景,写进开发规范里
- 再定字段:确定必选字段和可选字段,做成模板
- 统一输出:用日志框架(比如 Logback、Log4j2)的 PatternLayout 或者 JSON 编码器,保证格式统一
- 自动化检查:在 CI 流程里加一个日志格式检查,不符合规范的不让合并
- 定期复盘:每季度看看日志量是不是合理,有没有不必要的日志在刷屏
💡 一个小技巧:我个人习惯在日志框架里配置一个「动态级别开关」。线上默认只开 INFO 及以上,但遇到疑难问题时,可以通过配置中心动态开启某个模块的 DEBUG 日志,不用重启服务。这个功能在排查问题时特别管用。
日志格式设计这事儿,说白了就是「先立规矩,再自动化」。规矩立好了,后面排查问题、做数据分析,都会轻松很多。别等到出事了才想起来改,那时候就晚了。
公众号:蓝海资料掘金营,微信deep3321