3. 升级日志基础:日志级别、格式规范与存储轮转
日志这东西,平时没人看,一出问题它就是救命稻草。我在OTA项目里摸爬滚打这些年,可以说一半的故障定位时间都花在日志分析上。日志写得好不好,直接决定了你是花10分钟解决问题,还是花一整天抓耳挠腮。
今天咱们就把日志的基础打扎实。说白了,就是三个事:日志该怎么分级、格式怎么写才规范、存下来的日志怎么管理。
3.1 日志级别:DEBUG / INFO / WARN / ERROR
日志级别不是随便定的。我见过不少团队,所有日志都用INFO,结果一刷屏全是废话,真正有用的信息全淹没了。你想想看,这跟没写有什么区别?
我个人习惯把这四个级别当成「严重程度」和「关注频率」的组合。咱们一个一个说。
| 级别 | 含义 | 典型场景 | 我建议的使用频率 |
|---|---|---|---|
| DEBUG | 调试信息,开发阶段用 | 变量值、函数入口/出口、中间状态 | 开发期全开,发布后默认关闭 |
| INFO | 正常流程的关键节点 | 升级开始、下载完成、校验通过、重启 | 始终保留,但别刷屏 |
| WARN | 异常但可恢复 | 重试、超时、校验失败后重试、网络波动 | 始终保留,重点关注 |
| ERROR | 不可恢复的故障 | 升级失败、分区写入失败、签名校验不通过 | 必须记录,触发告警 |
这里有个坑,我踩过。有一次项目上线,同事把所有日志都打成了ERROR。结果呢?告警系统直接炸了,一晚上几百条告警,运维直接无视了。等到真出问题的时候,没人注意到。嗯,这就是「狼来了」的教训。
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锁定了两个线程互相等待。
- 文件名:行号:这个太重要了。没有它,你看到「校验失败」还得去翻代码找是哪一行。有了它,直接定位。
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缓冲区:掉电就丢,只能用于调试。
我建议的做法是:
- 正常运行时,日志写到RAM缓冲区,定期刷到Flash。
- 升级过程中,日志直接写到Flash,防止掉电丢失。
- 升级完成后,把关键日志同步到外部存储(如果有的话)。
这样既保证了性能,又保证了可靠性。
- 日志级别要严格区分,别把INFO当ERROR用。
- 格式统一,时间戳、模块、线程ID、行号一个都不能少。
- 轮转策略要提前设计好,别等Flash满了再想。
- 存储位置要考虑掉电保护和写入寿命。
好了,日志基础就这些。下一章咱们聊聊怎么从日志里快速定位升级失败的原因。到时候我会拿几个真实案例出来,手把手教你怎么看日志。