日志系统设计原则:日志分级、分类与格式规范

日志这东西,看着不起眼,但真到了线上排查问题的时候,你就知道它的重要性了。我做过不少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。这样排查问题的时候,你才能知道到底发生了什么。

一句话总结:日志不是写给机器看的,是写给未来的你(或者你的同事)看的。设计得好,省心省力;设计得差,欲哭无泪。