日志系统:日志等级定义、输出格式规范与存储轮转策略

日志系统,说白了就是设备的“黑匣子”。

我做了这么多年嵌入式,最怕的就是设备在现场出了问题,却连个像样的日志都拿不出来。你想想看,一个设备跑着跑着突然死机了,或者功能异常了,如果没有日志,你只能靠猜。猜来猜去,最后可能连问题出在哪都不知道。

所以,日志系统不是可有可无的装饰品,它是调试和排错的第一道防线。今天我们就来聊聊日志系统的三个核心要素:等级定义、输出格式、存储与轮转。

一、日志等级定义:DEBUG / INFO / WARN / ERROR

日志等级,说白了就是给日志贴个标签,告诉别人这条日志有多重要。

我个人习惯把日志分成四个等级,这也是业界最通用的做法:

等级 含义 使用场景
DEBUG 调试信息 开发阶段,用于跟踪变量值、函数调用流程等
INFO 普通信息 记录设备正常运行状态,如启动完成、连接成功
WARN 警告信息 出现非致命异常,设备仍可继续运行,但需关注
ERROR 错误信息 出现致命错误,设备功能受损或即将停止工作

这里有个坑,我必须要提醒你。

注意:千万不要把DEBUG日志带到正式发布版本中。我在项目中遇到过,某次发布时忘了关DEBUG日志,结果日志文件一天就写了几百兆,直接把Flash撑爆了。设备跑着跑着就挂了,查了半天才发现是日志惹的祸。

所以,我建议在代码里做一个编译开关,比如这样:

#define LOG_LEVEL LOG_LEVEL_INFO

#if LOG_LEVEL <= LOG_LEVEL_DEBUG
    #define LOG_DEBUG(fmt, ...) printf("[DEBUG] " fmt, ##__VA_ARGS__)
#else
    #define LOG_DEBUG(fmt, ...) // 空定义,编译时直接去掉
#endif

#if LOG_LEVEL <= LOG_LEVEL_INFO
    #define LOG_INFO(fmt, ...) printf("[INFO] " fmt, ##__VA_ARGS__)
#else
    #define LOG_INFO(fmt, ...)
#endif

这样,发布版本只需要把 LOG_LEVEL 改成 LOG_LEVEL_INFOLOG_LEVEL_WARN,DEBUG日志就自动消失了。干净利落。

二、日志输出格式规范

日志格式,决定了你拿到日志后能不能快速看懂。

我见过最糟糕的日志,就是光秃秃一句话:“连接失败”。谁连接的?什么时候失败的?失败原因是什么?全都没有。这种日志,说白了就是废纸。

一个规范的日志格式,至少应该包含以下信息:

  • 时间戳:精确到毫秒,最好带时区
  • 日志等级:DEBUG/INFO/WARN/ERROR
  • 模块名:比如 WIFI、BLE、SENSOR
  • 文件名+行号:方便定位代码位置
  • 日志内容:描述发生了什么

我个人习惯的格式是这样的:

[2025-03-21 14:30:45.123] [INFO] [WIFI] [wifi_manager.c:256] WiFi连接成功,SSID: MyHome, IP: 192.168.1.100

你看,一眼就能看出:什么时候、什么模块、什么等级、在哪行代码、发生了什么。这才是好日志。

小技巧:我建议在日志里加上一个“上下文ID”。比如每次请求或任务都分配一个唯一ID,这样多条日志就能串起来。我曾经用这个办法,在三天内定位了一个多线程竞争的死锁问题,否则可能得查一个月。

代码实现上,可以用一个宏来统一格式化:

#define LOG(level, module, fmt, ...) \
    do { \
        char timestamp[32]; \
        get_timestamp(timestamp, sizeof(timestamp)); \
        printf("[%s] [%s] [%s] [%s:%d] " fmt "\n", \
               timestamp, level, module, __FILE__, __LINE__, ##__VA_ARGS__); \
    } while(0)

三、日志存储与轮转策略

日志写好了,存哪?存多久?满了怎么办?

这些问题不解决,日志系统就是空中楼阁。

我建议分两种情况考虑:

1. 嵌入式设备(资源受限)

这类设备Flash空间有限,可能只有几MB。我的做法是:

  • 环形缓冲区:在RAM里开一个固定大小的缓冲区,新日志覆盖旧日志。掉电就丢,但速度快,不伤Flash。
  • Flash存储:只存ERROR和WARN级别的日志。INFO和DEBUG不存,因为太占空间。
  • 轮转策略:比如只保留最近3个日志文件,每个文件256KB。满了就删最旧的。

代码示例:

#define LOG_FILE_MAX_SIZE (256 * 1024)  // 256KB
#define LOG_FILE_MAX_COUNT 3

void log_write_to_flash(const char *log_entry) {
    // 检查当前文件大小
    if (get_file_size(LOG_CURRENT_FILE) > LOG_FILE_MAX_SIZE) {
        // 轮转:删除最旧文件,创建新文件
        rotate_log_files();
    }
    // 追加写入
    append_to_file(LOG_CURRENT_FILE, log_entry);
}
注意:Flash有写入寿命限制,一般10万次左右。如果每秒写一次日志,几天就报废了。所以,我建议在日志写入前加一个“去重”或“限频”机制。比如同一个错误,1分钟内只记录一次。

2. 上位机或服务器(资源充足)

这类场景下,空间不是问题,但管理是问题。我建议:

  • 按天分文件:比如 log_2025-03-21.txt
  • 按大小轮转:单个文件超过100MB就自动分割
  • 定期清理:保留最近30天的日志,更早的自动删除或压缩归档

嗯,这里要注意一点:日志轮转时,一定要保证写入操作的原子性。我曾经遇到过,轮转过程中设备突然断电,结果新旧文件都损坏了,日志全丢。后来我改用“先写临时文件,再重命名”的方式,才彻底解决这个问题。

知识体系总览

下面这张图,是我对日志系统核心逻辑的总结。你可以把它当作一个检查清单,看看自己的日志系统有没有遗漏。

日志系统核心逻辑 日志等级定义 输出格式规范 存储与轮转策略 DEBUG / INFO / WARN / ERROR 时间戳 + 等级 + 模块 + 位置 环形缓冲区 / Flash / 按天轮转 核心原则 1. 等级分明:不同场景使用不同等级,避免信息过载 2. 格式统一:所有日志遵循同一模板,方便解析和检索 3. 轮转可靠:保证写入原子性,防止日志丢失或损坏

好了,关于日志系统的三个核心要素,我就讲到这里。记住,日志不是写给机器看的,是写给人看的。你写的每一条日志,都是在帮未来的自己(或者你的同事)省时间。所以,认真对待它。


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