日志系统设计原则:日志分级、分类与格式规范
日志这东西,看着不起眼,但真到了线上排查问题的时候,你就知道它的重要性了。我做过不少TBOX项目,坦白讲,日志系统设计得好不好,直接决定了你晚上能不能睡个安稳觉。
今天我们就来聊聊日志系统的三个核心设计原则:分级、分类、格式规范。这三个东西,说白了就是给日志定规矩——什么级别的日志该记?什么类型的日志放哪?日志长什么样?
一、日志分级:DEBUG/INFO/WARN/ERROR/FATAL
日志分级,我个人的习惯是分成五级。你想想看,如果所有日志都混在一起,那跟没记有什么区别?
| 级别 | 含义 | 典型场景 | 是否上报云端 |
|---|---|---|---|
| DEBUG | 调试信息 | 函数入口/出口、变量值 | 否(本地保留) |
| INFO | 正常信息 | 模块启动、连接成功 | 是(按需) |
| WARN | 警告信息 | 重试、超时、降级 | 是 |
| ERROR | 错误信息 | 功能失败、数据异常 | 是(立即) |
| FATAL | 致命错误 | 系统崩溃、看门狗复位 | 是(紧急) |
嗯,这里要注意:DEBUG日志绝对不能上生产环境。我在项目中遇到过,有人把DEBUG日志开到了线上,结果一天刷了几百兆日志,直接把Flash写坏了。你说冤不冤?
核心原则:生产环境只保留INFO及以上级别。DEBUG日志只在开发调试时开启,通过宏或编译开关控制。
二、日志分类:系统/应用/通信/安全
分级是纵向的,分类是横向的。我习惯把TBOX的日志分成四大类:
- 系统日志:操作系统、驱动、硬件状态。比如内存使用率、CPU负载、文件系统错误。
- 应用日志:业务逻辑、功能模块。比如远程控制、OTA升级、定位服务。
- 通信日志:网络连接、协议交互。比如MQTT收发、TCP重连、CAN报文。
- 安全日志:认证、加密、异常访问。比如TLS握手失败、证书过期、非法请求。
为什么要分这么细?我举个例子。有一次客户反馈TBOX经常掉线,我第一反应是看通信日志。结果发现是安全日志里记录了证书过期,导致TLS握手失败。如果所有日志混在一起,你根本不知道从哪查起。
我的建议:每个日志条目都带上分类标签。比如 [SYS]、[APP]、[COM]、[SEC]。这样grep的时候一搜一个准。
三、日志格式规范:统一标准
日志格式,说白了就是让机器能解析、让人能看懂。我见过最头疼的日志,就是那种「自由发挥」的——有人用中文,有人用英文,有人连时间戳都不带。
我个人推荐的标准格式如下:
[时间戳] [级别] [分类] [模块] [线程ID] 日志内容 | 附加字段
举个例子:
2025-01-15 14:30:22.456 [ERROR] [COM] [MQTT] [0x1234] 连接服务器失败,重试第3次 | server=iot.xxx.com, retry=3, errno=110
你看,这个日志包含了什么信息?
- 时间精确到毫秒——方便定位问题发生的时间点
- 级别是ERROR——说明需要关注
- 分类是COM——属于通信模块
- 模块是MQTT——具体到子模块
- 线程ID——方便排查多线程问题
- 日志内容——描述发生了什么
- 附加字段——关键参数,方便自动化分析
避坑指南:我曾经遇到过一个项目,日志里没有线程ID。结果多线程并发写日志,日志内容互相穿插,根本没法看。后来我强制要求每个日志都带上线程ID,问题迎刃而解。
四、代码示例:日志宏定义
说了这么多,直接上代码。这是我常用的日志宏定义:
// log.h
#define LOG_DEBUG(tag, fmt, ...) \
log_write(LOG_LEVEL_DEBUG, tag, "[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_INFO(tag, fmt, ...) \
log_write(LOG_LEVEL_INFO, tag, fmt, ##__VA_ARGS__)
#define LOG_WARN(tag, fmt, ...) \
log_write(LOG_LEVEL_WARN, tag, fmt, ##__VA_ARGS__)
#define LOG_ERROR(tag, fmt, ...) \
log_write(LOG_LEVEL_ERROR, tag, fmt, ##__VA_ARGS__)
#define LOG_FATAL(tag, fmt, ...) \
log_write(LOG_LEVEL_FATAL, tag, fmt, ##__VA_ARGS__)
使用示例:
// 应用模块
LOG_INFO("[APP]", "OTA升级开始,版本号: %s", version);
// 通信模块
LOG_ERROR("[COM]", "MQTT连接失败,errno: %d", errno);
// 安全模块
LOG_WARN("[SEC]", "证书即将过期,剩余天数: %d", days);
嗯,这里要注意:DEBUG日志里我习惯加上文件名和行号,方便快速定位代码位置。但INFO及以上级别就不需要了,减少日志体积。
五、总结与建议
日志系统设计,说白了就是三个字:规矩。分级让你知道轻重缓急,分类让你知道从哪查起,格式规范让机器和人都不迷糊。
最后分享一个我踩过的坑:日志不要只记成功,不记失败。有一次排查问题,发现日志里全是「连接成功」「发送成功」,但就是找不到失败记录。后来才发现,代码里只在成功路径打了日志,失败路径直接return了。你说这日志还有什么用?
所以我的习惯是:每个关键路径,成功和失败都要打日志。成功打INFO,失败打ERROR。这样排查问题的时候,你才能知道到底发生了什么。
一句话总结:日志不是写给机器看的,是写给未来的你(或者你的同事)看的。设计得好,省心省力;设计得差,欲哭无泪。