日志级别设计:TRACE、DEBUG、INFO、WARN、ERROR、FATAL 的定义与使用场景

日志级别这东西,说白了就是给日志信息贴个标签。告诉系统:「这条消息有多重要?」。我见过不少团队,上来就 INFO 一把梭,结果线上出问题,日志文件里全是废话,真正有用的信息反而被淹没了。

今天咱们就把这六个级别掰开揉碎,讲清楚每个级别该什么时候用,什么时候不该用。

一、六个级别的定义与使用场景

先看一张总览表,心里有个底:

级别 数值 含义 典型场景
TRACE 0 最细粒度的追踪信息 函数调用链、变量值变化、循环每次迭代
DEBUG 1 调试信息,帮助定位问题 模块入口/出口、关键分支判断、中间计算结果
INFO 2 常规运行信息 系统启动、任务完成、状态切换、配置加载
WARN 3 潜在问题,不影响当前运行 资源即将耗尽、重试次数超限、配置项缺失使用默认值
ERROR 4 错误,功能受影响但系统可继续 通信失败、文件读写错误、传感器数据异常
FATAL 5 致命错误,系统无法继续运行 内存分配失败、看门狗超时、硬件自检失败
核心原则:数值越大,严重程度越高。生产环境通常只开启 WARN 及以上级别,DEBUG 和 TRACE 只在开发或定位问题时启用。

二、每个级别的「潜规则」

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);
}
注意:TRACE 日志量极大。我曾经在一个项目中,TRACE 级别下每秒产生 5000+ 条日志,直接把 Flash 写穿了。所以 TRACE 必须通过编译开关或运行时开关控制,绝不能在正式产品中默认开启。

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 图展示了日志级别的金字塔结构,以及对应的使用场景:

日志级别金字塔与使用场景 FATAL 系统崩溃,最后遗言 ERROR 功能受损,需定位 WARN 潜在风险,预警 INFO 正常运行里程碑 DEBUG 开发调试,定位问题 TRACE 最细粒度追踪 严重程度递增 日志量递增 生产环境默认开启 WARN 及以上级别,DEBUG/TRACE 仅在开发或排障时启用

五、避坑指南

我的经验:我曾经在一个物联网项目中,把传感器校准参数打印在 INFO 级别。结果每 10 秒输出一次,一天产生 800KB 日志。后来改成只在启动时打印一次,日志量直接降到 2KB/天。所以,高频重复的日志一定要降级或限速
我曾经踩过的坑:有一次线上设备频繁重启,我远程开启 DEBUG 级别想查原因。结果 DEBUG 日志量太大,把剩余的 Flash 空间写满了,设备直接变砖。从那以后,我强制要求:日志系统必须预留至少 20% 的存储空间作为安全余量,并且要有「日志满时自动覆盖最旧日志」的机制。

好了,日志级别设计就聊到这儿。记住一句话:TRACE 和 DEBUG 是开发者的工具,INFO 是运维者的眼睛,WARN 是预警铃,ERROR 是报警器,FATAL 是墓碑。 用对级别,你的日志系统才能真正帮你「看见」系统的运行状态。


公众号:蓝海资料掘金营,微信deep3321