日志基础:日志级别与结构化日志设计
日志这东西,说白了就是系统的「黑匣子」。我刚开始做运维那会儿,觉得日志嘛,能打出来就行。直到有一次线上故障,翻了几万行日志全是 INFO,关键错误被淹没了——嗯,那次教训让我彻底明白了日志级别的重要性。
一、日志级别的正确打开方式
先说说五个标准级别。别看它们简单,用错了会出大问题。
| 级别 | 含义 | 我建议的使用场景 |
|---|---|---|
| DEBUG | 调试信息 | 开发环境、问题排查时临时开启 |
| INFO | 正常流程 | 用户登录、订单创建、定时任务执行 |
| WARN | 潜在问题 | 磁盘使用率超过80%、重试次数超过阈值 |
| ERROR | 功能异常 | 数据库连接失败、API调用返回500 |
| FATAL | 致命错误 | 内存溢出、配置加载失败导致服务无法启动 |
⚠️ 我曾经踩过的坑: 把业务异常直接打成了 FATAL。结果告警系统每分钟都在报警,运维同事差点把我拉黑。FATAL 只留给「服务活不下去了」的场景,其他异常用 ERROR 就够了。
二、级别使用原则
我个人习惯遵循「金字塔原则」:
- INFO 要克制:每个请求只打一条核心日志,别把 SQL 查询结果都打出来
- WARN 要敏感:任何可能演变成问题的苗头,都值得记录
- ERROR 要完整:必须包含上下文信息,比如请求ID、参数、堆栈
- FATAL 要极少:一年出现个位数次,才算正常
你想想看,如果 ERROR 日志一天几十万条,那跟没有告警有什么区别?
三、结构化日志设计
传统日志长这样:
2024-01-15 10:30:22 ERROR 用户下单失败:null
这种日志,说白了就是废的。你根本不知道哪个用户、哪个订单、为什么失败。
结构化日志就不一样了:
{
"timestamp": "2024-01-15T10:30:22.123Z",
"level": "ERROR",
"logger": "OrderService",
"traceId": "a1b2c3d4",
"userId": 10086,
"orderId": "ORD20240115001",
"message": "用户下单失败",
"error": {
"type": "StockInsufficient",
"detail": "商品ID: 9527, 库存: 0, 请求: 1"
},
"duration": 2345
}
核心要点: 结构化日志必须包含 traceId、时间戳、级别、业务关键字段。这样你才能在 ELK 里快速聚合、过滤、分析。
四、知识体系结构图
下面这张图,是我自己总结的日志设计核心逻辑:
五、实战中的避坑指南
💡 我的经验: 日志级别一定要支持运行时动态调整。我曾经在排查问题时,需要临时开启 DEBUG 日志,但又不想重启服务。后来我们给日志框架加了个 API,通过配置中心就能实时切换级别。
还有几个细节要注意:
- 不要打印敏感信息:密码、身份证号、银行卡号,一律脱敏
- 控制日志大小:单条日志建议不超过 1KB,太长会影响性能
- 统一时间格式:全系统用 ISO 8601,别有的用时间戳有的用字符串
- ERROR 必须带堆栈:但别把整个堆栈打满,截取前 10 行就够了
⚠️ 我曾经犯过的错: 在循环里打 DEBUG 日志,结果日志量暴增 100 倍,直接把磁盘打满了。从那以后,我要求所有循环内的日志必须加频率限制。
六、代码示例
以 Java 的 Logback 为例,一个合理的配置:
<!-- logback-spring.xml -->
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
<!-- 业务模块可以单独调级别 -->
<logger name="com.example.order" level="DEBUG" additivity="false">
<appender-ref ref="JSON"/>
</logger>
</configuration>
代码里怎么打日志?我推荐这样:
// 好的做法
log.info("用户下单成功",
kv("userId", userId),
kv("orderId", orderId),
kv("amount", amount));
// 不好的做法
log.info("用户" + userId + "下单成功,订单号:" + orderId);
前者是结构化日志,后者是字符串拼接。你想想看,在 ELK 里搜索 userId=10086 和搜索「用户10086下单成功」,哪个更方便?
嗯,日志级别和结构化设计,说白了就是「在正确的地方,用正确的格式,记录正确的信息」。别小看这基础功夫,我见过太多系统因为日志没设计好,排查问题要多花几倍时间。