3、日志格式规范:结构化日志 vs 非结构化日志

聊到日志格式,我见过太多团队在这个问题上栽跟头了。

早期做嵌入式开发时,大家习惯用 printf 随手打点信息。那时候觉得「能看就行」。直到有一次,产品上线后出现偶发死机,几千行日志堆在一起,我盯着屏幕看了整整两天,愣是没找出规律。嗯,从那以后,我对日志格式就有了执念。

3.1 非结构化日志:方便一时,痛苦一世

说白了,非结构化日志就是自由文本。比如:

[INFO] device init ok
[WARN] temp too high: 85
[ERROR] can't open file

这种格式有什么问题?

  • 机器没法解析——你想用脚本统计一下「最近一小时有多少次温度告警」?得写正则,还得祈祷没人改过措辞。
  • 字段缺失——「85」是摄氏度还是华氏度?哪个模块报的?线程 ID 是多少?全靠猜。
  • 搜索效率低——我在项目中遇到过,用 grep 搜一个错误码,结果把注释里的也搜出来了,误判了两次。
我曾经踩过的坑: 有一次客户反馈设备频繁重启,我远程拉回日志,发现里面混杂了中英文、甚至还有乱码。原因是不同工程师用了不同的编码格式写日志。从那以后,我强制团队统一用纯 ASCII 输出,所有非结构化日志一律禁止上线。

3.2 结构化日志:让机器读懂你的日志

结构化日志,就是把每条日志变成一个「键值对」集合。你想想看,如果日志长这样:

{"level":"ERROR","time":1700000000,"module":"sensor","thread":1234,"msg":"temp too high","value":85,"unit":"C"}

是不是一目了然?

我个人习惯用 JSON 格式,原因很简单:

  • 几乎所有日志分析工具都原生支持 JSON 解析
  • 字段可以随意扩展,不会破坏已有解析逻辑
  • 人类可读性也不错,至少比二进制强

但要注意,嵌入式环境资源有限。JSON 解析库会占用额外的 RAM 和 Flash。我建议在资源充足的 MCU(比如 Cortex-M4 以上)上使用,如果是 8 位单片机,可以考虑更紧凑的格式,比如 Key=Value 形式:

level=ERROR time=1700000000 module=sensor thread=1234 msg="temp too high"

这种格式解析起来几乎零开销,用 sscanf 就能搞定。

3.3 核心字段设计:我推荐这 6 个

不管用哪种格式,有些字段是必须的。我根据多年经验,总结了一个「黄金六件套」:

字段 类型 说明 示例
时间戳 uint64 Unix 时间戳,微秒级 1700000000123456
日志级别 enum DEBUG/INFO/WARN/ERROR/FATAL ERROR
模块名 string 不超过 16 字符 sensor
线程 ID uint16 RTOS 中的任务句柄 1234
消息体 string 人类可读的描述 temp too high
上下文数据 key=value 附加的数值/状态 value=85 unit=C
关于时间戳,我有个建议: 不要用人类可读的字符串(比如 "2024-01-01 12:00:00"),它占空间、解析慢。直接用 Unix 时间戳,微秒级精度。为什么是微秒?因为很多嵌入式场景下,毫秒级不够用。我在调试一个电机控制算法时,就是靠微秒级时间戳才定位到两个任务之间的竞态条件。

3.4 模块名与线程 ID 的设计细节

模块名:我习惯用两级命名,比如 sensor.tempnetwork.wifi。这样在日志分析时,可以按模块前缀快速过滤。注意长度限制,16 字节以内,否则在日志缓冲区里容易截断。

线程 ID:在 RTOS 中,每个任务都有一个唯一的句柄。我建议直接输出这个句柄的数值,而不是任务名字符串。为什么?因为任务名可能重复(比如多个同名任务),而句柄是唯一的。我曾经在一个项目中,两个任务都叫 "worker",日志里根本分不清是谁报的错。改成线程 ID 后,问题迎刃而解。

3.5 知识体系图:结构化日志的核心逻辑

下面这张图,是我自己总结的结构化日志设计思路。你可以把它当作一个检查清单:

结构化日志设计核心逻辑 日志格式规范 非结构化日志(自由文本) 缺点:机器难解析、字段缺失 结构化日志(键值对) 优点:机器可解析、可扩展、易搜索 核心字段(黄金六件套) • 时间戳(Unix 微秒) • 日志级别(DEBUG~FATAL) • 模块名(两级命名,≤16字节) • 线程 ID(RTOS 句柄) • 消息体(人类可读) • 上下文数据(key=value)

3.6 避坑指南:我踩过的几个坑

  • 时间戳精度不够——我曾经用毫秒级时间戳,结果两个事件间隔只有 200 微秒,日志里显示为同一毫秒,根本分不清先后。后来统一改成微秒级。
  • 模块名太长——有人把模块名写成 "sensor_temperature_controller",结果日志缓冲区溢出,后面的字段全丢了。我规定模块名不超过 16 字节,超长就截断。
  • 线程 ID 用字符串——前面说过,任务名可能重复。直接用数值句柄,简单可靠。
  • 日志缓冲区太小——结构化日志每条可能上百字节,如果缓冲区只有 256 字节,高并发下日志会频繁丢失。我建议至少 2KB 起步。
一个小技巧: 在开发阶段,可以用 JSON 格式方便调试。到了量产阶段,可以切换到更紧凑的二进制格式(比如 Protocol Buffers 或自定义的 TLV 格式),节省带宽和存储。我一般会在代码里留一个编译开关,一键切换。

3.7 总结一下

结构化日志不是锦上添花,而是雪中送炭。你想想看,当设备出问题时,你能依赖的就是那几行日志。如果日志格式一团糟,排查问题就像大海捞针。

我个人建议,从项目第一天就强制使用结构化日志。哪怕前期多花两天时间搭框架,也比后期花两周时间重构要划算得多。

嗯,关于日志格式就聊到这里。记住那六个字段,它们能帮你省下无数个熬夜排查的夜晚。

专注资料整理