日志级别设计:TRACE、DEBUG、INFO、WARN、ERROR、FATAL 的定义与使用场景
日志级别这东西,说白了就是给日志信息贴个标签。告诉系统:「这条消息有多重要?」。我见过不少团队,上来就 INFO 一把梭,结果线上出问题,日志文件里全是废话,真正有用的信息反而被淹没了。
今天咱们就把这六个级别掰开揉碎,讲清楚每个级别该什么时候用,什么时候不该用。
一、六个级别的定义与使用场景
先看一张总览表,心里有个底:
| 级别 | 数值 | 含义 | 典型场景 |
|---|---|---|---|
| TRACE | 0 | 最细粒度的追踪信息 | 函数调用链、变量值变化、循环每次迭代 |
| DEBUG | 1 | 调试信息,帮助定位问题 | 模块入口/出口、关键分支判断、中间计算结果 |
| INFO | 2 | 常规运行信息 | 系统启动、任务完成、状态切换、配置加载 |
| WARN | 3 | 潜在问题,不影响当前运行 | 资源即将耗尽、重试次数超限、配置项缺失使用默认值 |
| ERROR | 4 | 错误,功能受影响但系统可继续 | 通信失败、文件读写错误、传感器数据异常 |
| FATAL | 5 | 致命错误,系统无法继续运行 | 内存分配失败、看门狗超时、硬件自检失败 |
二、每个级别的「潜规则」
1. TRACE —— 最细的「显微镜」
TRACE 是用来追踪代码执行路径的。我个人习惯在以下场景使用:
- 函数调用链的进入和退出
- 循环体内部每次迭代的关键变量
- 状态机的每个状态转换
举个例子,一个简单的状态机:
void state_machine_run(void) {
log_trace("State machine enter, current_state=%d", current_state);
switch (current_state) {
case STATE_IDLE:
log_trace("STATE_IDLE: checking trigger...");
// ...
break;
case STATE_RUNNING:
log_trace("STATE_RUNNING: processing data...");
// ...
break;
}
log_trace("State machine exit, next_state=%d", next_state);
}
2. DEBUG —— 开发者的「听诊器」
DEBUG 比 TRACE 粗一些,但依然属于「开发阶段专用」。我建议在以下位置使用:
- 模块的入口和出口(打印入参和返回值)
- 关键分支判断(if-else 走了哪条路)
- 中间计算结果(比如滤波后的值、校验和)
你想想看,如果线上出了 bug,你连函数有没有被调用都不知道,那排查起来得多痛苦?DEBUG 日志就是帮你确认「代码确实执行到了这里」。
3. INFO —— 系统的「日记本」
INFO 记录的是系统正常运行时的「里程碑」事件。比如:
- 系统启动完成,版本号、编译时间
- 任务创建成功、定时器启动
- 配置加载完成,关键参数值
- 周期性上报的心跳、状态摘要
INFO 日志应该让人一眼就能看出系统「活得好不好」。我在项目中遇到过,有人把传感器每次采样的值都打 INFO,结果一天产生 2GB 日志。嗯,这其实应该用 DEBUG 或 TRACE。
4. WARN —— 温柔的「提醒」
WARN 表示「有问题,但系统还能扛」。比如:
- 内存使用率超过 80%
- 通信重试第 2 次、第 3 次
- 配置项缺失,使用默认值
- 传感器数据略超范围,但还在容忍区间
WARN 是运维人员的「预警灯」。如果 WARN 太多,说明系统在亚健康运行,需要关注了。
5. ERROR —— 出了「事故」
ERROR 表示功能受损,但系统还能继续跑。比如:
- 文件打开失败,跳过该文件继续处理下一个
- 通信超时,重试 3 次后放弃
- 传感器读取失败,使用上一次的有效值
ERROR 日志必须包含足够的信息来定位问题:错误码、上下文、时间戳。我曾经见过一条 ERROR 日志只写了「Error occurred」,这跟没写有什么区别?
6. FATAL —— 最后的「遗言」
FATAL 是系统崩溃前的最后一条日志。比如:
- malloc 返回 NULL,且无法降级运行
- 看门狗即将触发复位
- 硬件自检失败,无法启动
FATAL 日志必须立即 flush 到存储介质,不能缓冲。因为系统马上就要挂了,缓冲区的数据可能来不及写入。
三、如何避免日志泛滥?
日志泛滥是嵌入式系统的「慢性病」。我见过最夸张的项目,一个 256KB Flash 的 MCU,日志一天写满 3 次。怎么治?
1. 分级开关,编译时裁剪
最有效的手段:通过宏定义在编译时决定哪些级别生效。
#define LOG_LEVEL LOG_LEVEL_WARN
#if LOG_LEVEL <= LOG_LEVEL_TRACE
#define log_trace(fmt, ...) printf("[TRACE] " fmt "\n", ##__VA_ARGS__)
#else
#define log_trace(fmt, ...) ((void)0)
#endif
// 同理定义 DEBUG、INFO、WARN、ERROR、FATAL
这样,生产固件中 TRACE 和 DEBUG 的代码根本不会编译进去,零开销。
2. 运行时动态调整
有些场景需要在运行时切换级别。比如通过串口命令:
void set_log_level(int level) {
if (level >= LOG_LEVEL_TRACE && level <= LOG_LEVEL_FATAL) {
g_log_level = level;
log_info("Log level changed to %d", level);
}
}
这样,现场工程师可以在不重启设备的情况下,临时开启 DEBUG 级别来定位问题。
3. 限速与采样
对于高频重复的日志(比如每 10ms 一次的传感器采样),使用限速机制:
static uint32_t last_log_time = 0;
if (now - last_log_time > 1000) { // 每秒最多打一次
log_warn("Sensor read failed, count=%d", fail_count);
last_log_time = now;
}
4. 日志内容精简
日志内容越短越好。能用数字 ID 就别用字符串,能用缩写就别写全称。
// 不推荐
log_error("The temperature sensor on I2C bus 1 address 0x48 read failed with error code 0x05");
// 推荐
log_error("TEMP_SENSOR_ERR: bus=1, addr=0x48, err=0x05");
5. 环形缓冲区 + 按需导出
对于 RAM 充足的系统,使用环形缓冲区存储最近 N 条日志。当发生 ERROR 或 FATAL 时,将缓冲区内容导出到 Flash 或串口。这样既保留了现场信息,又避免了持续写 Flash。
四、一张图总结核心逻辑
下面这张 SVG 图展示了日志级别的金字塔结构,以及对应的使用场景:
五、避坑指南
好了,日志级别设计就聊到这儿。记住一句话:TRACE 和 DEBUG 是开发者的工具,INFO 是运维者的眼睛,WARN 是预警铃,ERROR 是报警器,FATAL 是墓碑。 用对级别,你的日志系统才能真正帮你「看见」系统的运行状态。
公众号:蓝海资料掘金营,微信deep3321