3. 升级日志基础:日志级别、格式规范与存储轮转

日志这东西,平时没人看,一出问题它就是救命稻草。我在OTA项目里摸爬滚打这些年,可以说一半的故障定位时间都花在日志分析上。日志写得好不好,直接决定了你是花10分钟解决问题,还是花一整天抓耳挠腮。

今天咱们就把日志的基础打扎实。说白了,就是三个事:日志该怎么分级、格式怎么写才规范、存下来的日志怎么管理。

3.1 日志级别:DEBUG / INFO / WARN / ERROR

日志级别不是随便定的。我见过不少团队,所有日志都用INFO,结果一刷屏全是废话,真正有用的信息全淹没了。你想想看,这跟没写有什么区别?

我个人习惯把这四个级别当成「严重程度」和「关注频率」的组合。咱们一个一个说。

级别 含义 典型场景 我建议的使用频率
DEBUG 调试信息,开发阶段用 变量值、函数入口/出口、中间状态 开发期全开,发布后默认关闭
INFO 正常流程的关键节点 升级开始、下载完成、校验通过、重启 始终保留,但别刷屏
WARN 异常但可恢复 重试、超时、校验失败后重试、网络波动 始终保留,重点关注
ERROR 不可恢复的故障 升级失败、分区写入失败、签名校验不通过 必须记录,触发告警

这里有个坑,我踩过。有一次项目上线,同事把所有日志都打成了ERROR。结果呢?告警系统直接炸了,一晚上几百条告警,运维直接无视了。等到真出问题的时候,没人注意到。嗯,这就是「狼来了」的教训。

避坑指南: 我曾经在一个车机项目里,看到有人把「下载进度99%」这种信息也打成了WARN。理由是「怕进度卡住」。这完全不对。WARN应该留给那些「虽然能继续跑,但已经不对劲了」的情况。比如重试了3次才成功,这才是WARN。

3.2 日志格式规范

格式这东西,看起来是小事,但真到排查问题的时候,格式不统一会让你想砸键盘。我见过最离谱的日志,连时间戳都没有,就一行「升级失败」。你告诉我这怎么查?

我个人推荐的标准格式是这样的:

[时间戳] [级别] [模块] [线程ID] [文件名:行号] - 消息内容

举个例子:

[2025-03-15 14:32:18.456] [INFO] [OTA_DOWNLOAD] [0x1234] [ota_download.c:256] - 开始下载固件包,URL: https://ota.example.com/firmware.bin
[2025-03-15 14:32:20.123] [WARN] [OTA_DOWNLOAD] [0x1234] [ota_download.c:312] - 下载超时,第2次重试,已重试次数: 2/3
[2025-03-15 14:32:25.789] [ERROR] [OTA_VERIFY] [0x1234] [ota_verify.c:89] - 签名校验失败,期望值: 0xAABBCCDD,实际值: 0x11223344

为什么要这么写?我一个个解释:

  • 时间戳:精确到毫秒。OTA升级中,很多问题跟时序有关,比如「下载完成」和「开始校验」之间差了多久,这能帮你判断是不是有阻塞。
  • 级别:一目了然,方便过滤。
  • 模块:OTA升级涉及下载、校验、解压、写入、回滚等多个模块。没有模块名,你都不知道问题出在哪一环。
  • 线程ID:嵌入式系统里多线程是常态。我记得有一次排查死锁问题,就是靠线程ID锁定了两个线程互相等待。
  • 文件名:行号:这个太重要了。没有它,你看到「校验失败」还得去翻代码找是哪一行。有了它,直接定位。
小技巧: 我习惯在日志里加上「上下文ID」。比如一次OTA升级,从开始到结束,所有日志都带上同一个升级会话ID。这样你 grep 一下,整条链路就出来了。不用在几十万行日志里大海捞针。

3.3 日志存储与轮转策略

嵌入式设备存储空间有限,日志不能无限写下去。你想想看,一个设备跑几个月,日志能把Flash撑爆。所以日志轮转是必须的。

我常用的策略有两种:

3.3.1 基于文件大小的轮转

这是最常用的方式。设定一个最大文件大小,比如1MB。日志写满了,就自动切一个新文件。旧文件保留N个,超过N个就删最早的。

// 伪代码示例
#define LOG_FILE_MAX_SIZE  (1024 * 1024)  // 1MB
#define LOG_FILE_MAX_COUNT 5              // 保留5个文件

void log_rotate() {
    // 检查当前日志文件大小
    if (get_file_size(current_log) >= LOG_FILE_MAX_SIZE) {
        // 删除最旧的文件
        remove("ota_log.5");
        // 依次重命名: .4 -> .5, .3 -> .4, ...
        rename("ota_log.3", "ota_log.4");
        rename("ota_log.2", "ota_log.3");
        rename("ota_log.1", "ota_log.2");
        rename("ota_log.0", "ota_log.1");
        // 创建新文件
        create("ota_log.0");
    }
}

这样你始终有最近5MB的日志。对于大多数OTA故障排查来说,足够了。

3.3.2 基于时间的轮转

有些场景下,按时间切更合适。比如每天凌晨切一次,保留最近7天的日志。

[2025-03-15 00:00:00] - 日志轮转: 创建新文件 ota_log_20250315.log
[2025-03-16 00:00:00] - 日志轮转: 创建新文件 ota_log_20250316.log
// 保留7天,超过的自动删除

我个人更推荐按大小轮转。为什么?因为OTA升级不是每天都有。如果一周没升级,按时间轮转的日志全是空的,浪费空间。按大小轮转则更灵活,有日志就写,没日志就不占空间。

注意: 我曾经在一个项目里,日志轮转没处理好,导致升级过程中日志文件被锁住了,写入失败。结果升级失败后,连失败日志都没留下来。后来我加了个机制:升级开始前,先强制轮转一次,确保当前日志文件是空的,不会被锁。

3.4 日志存储的硬件考量

嵌入式设备上,日志存哪是个问题。常见的选择有:

  • Flash分区:专门划一个分区给日志。优点是速度快,缺点是Flash有写入寿命限制。
  • 外部SD卡:空间大,但掉电可能丢数据。
  • RAM缓冲区:掉电就丢,只能用于调试。

我建议的做法是:

  1. 正常运行时,日志写到RAM缓冲区,定期刷到Flash。
  2. 升级过程中,日志直接写到Flash,防止掉电丢失。
  3. 升级完成后,把关键日志同步到外部存储(如果有的话)。

这样既保证了性能,又保证了可靠性。

核心要点:
  • 日志级别要严格区分,别把INFO当ERROR用。
  • 格式统一,时间戳、模块、线程ID、行号一个都不能少。
  • 轮转策略要提前设计好,别等Flash满了再想。
  • 存储位置要考虑掉电保护和写入寿命。

好了,日志基础就这些。下一章咱们聊聊怎么从日志里快速定位升级失败的原因。到时候我会拿几个真实案例出来,手把手教你怎么看日志。