日志格式设计:时间戳、模块名、日志级别、文件名、行号、线程ID的标准化格式
日志格式这东西,看着简单,其实坑不少。我见过太多团队,日志随便写,出了bug根本没法查。今天咱们就把这个标准化格式彻底聊透。
为什么非要标准化?
你想想看,摄像头调试的时候,日志量是巨大的。一秒钟几十帧数据,每帧都可能产生好几条日志。如果没有统一格式,你拿什么去分析?
我个人习惯是,在项目启动第一天就把日志格式定死。为什么?因为后期改格式的成本太高了。我曾经在一个项目里,中途改过一次日志格式,结果所有历史日志都没法用脚本分析了,那叫一个痛苦。
标准格式长什么样?
先给你看一个我常用的格式模板:
[2025-01-15 14:23:45.678] [ISP] [ERROR] [sensor_drv.c:256] [TID:0x3A2F] Sensor i2c write timeout, reg=0x30, val=0x01
这条日志包含了所有关键信息。咱们一个一个拆开讲。
时间戳:精度要够,但别过度
时间戳是日志的灵魂。没有时间戳,你根本没法还原问题现场。
我建议用 YYYY-MM-DD HH:mm:ss.SSS 格式。毫秒精度对于摄像头调试来说基本够用。为什么不用微秒?说实话,大部分嵌入式系统的时钟精度达不到微秒级,强行用反而容易误导。
关键点:时间戳必须使用系统统一时钟源。我遇到过有人用不同定时器获取时间,结果日志时间顺序都是乱的,排查问题直接抓瞎。
模块名:别用缩写,用全称
模块名要清晰。比如 ISP、SENSOR、V4L2、MIPI、AE、AWB 这些。我见过有人用 M1、M2 这种代号,三个月后自己都忘了 M1 是啥。
模块名建议控制在 4-8 个字符,太长影响阅读,太短容易混淆。
日志级别:五个就够了
我常用的级别就五个:
| 级别 | 含义 | 使用场景 |
|---|---|---|
| FATAL | 致命错误 | 系统无法继续运行,必须重启 |
| ERROR | 错误 | 功能异常,但系统还能跑 |
| WARN | 警告 | 潜在问题,需要关注 |
| INFO | 信息 | 正常流程记录 |
| DEBUG | 调试 | 开发阶段使用,发布时关闭 |
小技巧:DEBUG 级别的日志在正式版本里一定要关掉。我曾经有个同事,DEBUG 日志没关,结果产品跑起来日志刷爆了 flash,系统直接死机。
文件名和行号:精确到行
这个太重要了。没有文件名和行号,你看到一条错误日志,还得满世界找代码。
我一般用 __FILE__ 和 __LINE__ 宏来自动获取。但注意,文件名最好只保留最后一级,比如 sensor_drv.c:256,而不是 /home/user/project/src/driver/sensor/sensor_drv.c:256。为什么?因为路径太长,日志一行都装不下。
线程ID:多线程调试的救命稻草
摄像头驱动里,线程多得很。主线程、ISP线程、传感器线程、中断处理线程...没有线程ID,你根本分不清日志是谁打的。
我建议用十六进制显示线程ID,比如 TID:0x3A2F。为什么用十六进制?因为系统工具(比如 top、ps)默认显示的就是十六进制,方便对照。
注意:线程ID要打印真实的系统线程ID,不要自己编一个。我见过有人用全局变量模拟线程ID,结果多线程并发时ID全乱了。
完整的日志宏定义
给你看看我常用的日志宏,直接复制就能用:
#define LOG_PRINT(level, module, fmt, ...) \
do { \
char timestamp[32]; \
get_timestamp(timestamp, sizeof(timestamp)); \
printf("[%s] [%s] [%s] [%s:%d] [TID:0x%04X] " fmt "\n", \
timestamp, module, level, \
__FILE__ + __builtin_strlen(__FILE__) - 20, \
__LINE__, \
(unsigned int)pthread_self(), \
##__VA_ARGS__); \
} while(0)
#define LOG_ERROR(module, fmt, ...) LOG_PRINT("ERROR", module, fmt, ##__VA_ARGS__)
#define LOG_WARN(module, fmt, ...) LOG_PRINT("WARN", module, fmt, ##__VA_ARGS__)
#define LOG_INFO(module, fmt, ...) LOG_PRINT("INFO", module, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(module, fmt, ...) LOG_PRINT("DEBUG", module, fmt, ##__VA_ARGS__)
这里有个细节:__FILE__ + __builtin_strlen(__FILE__) - 20 这行代码,作用是只保留文件名最后20个字符。嗯,这个技巧是我从老外代码里学来的,确实好用。
实际使用示例
假设你在调试 AE(自动曝光)模块,可以这样写:
LOG_INFO("AE", "Frame %d, exposure=%d, gain=%d", frame_id, exposure, gain);
LOG_WARN("AE", "Exposure out of range, clamping to %d", max_exposure);
LOG_ERROR("AE", "AE algorithm convergence failed after %d iterations", max_iter);
输出效果:
[2025-01-15 14:23:45.678] [AE] [INFO] [ae_algo.c:123] [TID:0x3A2F] Frame 1024, exposure=1500, gain=256
[2025-01-15 14:23:45.679] [AE] [WARN] [ae_algo.c:145] [TID:0x3A2F] Exposure out of range, clamping to 2000
[2025-01-15 14:23:45.680] [AE] [ERROR] [ae_algo.c:178] [TID:0x3A2F] AE algorithm convergence failed after 100 iterations
避坑指南
最后说几个我踩过的坑:
- 别用浮点数打印时间戳——嵌入式平台浮点运算慢,还容易有精度问题。用整数加格式化字符串更靠谱。
- 日志缓冲区要够大——我见过有人用 64 字节的缓冲区,结果一条日志就超了,直接截断。建议至少 256 字节。
- 多线程下加锁——printf 本身不是线程安全的。多个线程同时打日志,内容会混在一起。用互斥锁保护一下。
- 日志级别要可配置——运行时能动态调整日志级别,这个功能一定要有。不然每次改日志级别都要重新编译,太傻了。
好了,日志格式这块就这些。记住一句话:好的日志格式,能让调试效率翻倍;差的日志格式,能让你加班到怀疑人生。