日志格式设计:时间戳、模块名、日志级别、文件名、行号、线程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 本身不是线程安全的。多个线程同时打日志,内容会混在一起。用互斥锁保护一下。
  • 日志级别要可配置——运行时能动态调整日志级别,这个功能一定要有。不然每次改日志级别都要重新编译,太傻了。

好了,日志格式这块就这些。记住一句话:好的日志格式,能让调试效率翻倍;差的日志格式,能让你加班到怀疑人生。