5、日志格式定制:自定义日志格式(时间、级别、文件名、行号、函数名),ANSI颜色编码

日志系统好不好用,很大程度上取决于日志格式。你想想看,如果每条日志都长一个样,没有时间、没有位置信息,出问题了你怎么定位?

我个人习惯,日志格式至少要包含五个要素:时间、日志级别、文件名、行号、函数名。这五个信息凑齐了,排查问题基本就够用了。再加上 ANSI 颜色编码,终端里一眼就能看出哪些是错误、哪些是警告。

5.1 日志格式的组成要素

先说说这五个要素各自的作用:

要素 作用 示例
时间 记录事件发生的时刻,方便追溯 2025-03-21 14:30:45.123
日志级别 区分严重程度,快速筛选 [ERROR] / [WARN] / [INFO]
文件名 定位到源文件,缩小搜索范围 main.c / uart_driver.c
行号 精确到代码行,不用数行数 :42
函数名 知道是哪个函数出的问题 uart_send()

我在项目中遇到过,有些同事只打印一条字符串,连时间都没有。结果出问题后,大家对着日志猜了半天,谁也不知道是哪个模块、什么时候打印的。嗯,从那以后我就坚持把五个要素全带上。

5.2 使用宏自动获取位置信息

C 语言里,__FILE____LINE____func__ 这三个预定义宏,能帮我们自动获取文件名、行号和函数名。你不需要手动填写,编译器会帮你搞定。

#define LOG_INFO(fmt, ...) \
    log_write(LOG_LEVEL_INFO, __FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)

#define LOG_ERROR(fmt, ...) \
    log_write(LOG_LEVEL_ERROR, __FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)

你看,这样写日志的时候,只需要写 LOG_INFO("温度: %d", temp);,剩下的信息宏会自动填充。说白了,这就是用宏来偷懒,但偷得漂亮。

小提示:__FILE__ 返回的是完整路径,比如 /home/user/project/src/main.c。如果你只想显示文件名,可以用 strrchr() 找到最后一个 '/' 后面的部分。

5.3 时间戳格式化

时间格式我推荐用 YYYY-MM-DD HH:MM:SS.mmm,精确到毫秒。嵌入式系统里,很多问题发生在毫秒级别,光有秒级时间根本不够用。

#include <time.h>
#include <sys/time.h>

void get_timestamp(char *buf, size_t size) {
    struct timeval tv;
    struct tm *tm_info;

    gettimeofday(&tv, NULL);
    tm_info = localtime(&tv.tv_sec);

    strftime(buf, size, "%Y-%m-%d %H:%M:%S", tm_info);
    // 追加毫秒
    char ms[8];
    snprintf(ms, sizeof(ms), ".%03ld", tv.tv_usec / 1000);
    strncat(buf, ms, size - strlen(buf) - 1);
}

这里用了 gettimeofday() 获取微秒级时间,然后只取前三位作为毫秒。如果你用的 RTOS 没有这个函数,可以用系统滴答定时器自己算。

注意:localtime() 不是线程安全的。在多线程环境下,建议用 localtime_r() 替代。

5.4 ANSI 颜色编码

终端里黑底白字看久了,眼睛容易疲劳。加上颜色后,错误信息红色高亮,警告信息黄色提醒,调试信息灰色低调,阅读效率能提升不少。

ANSI 颜色码的格式是:\033[样式;前景色;背景色m。常用的颜色定义如下:

// 前景色
#define COLOR_RED     "\033[31m"
#define COLOR_GREEN   "\033[32m"
#define COLOR_YELLOW  "\033[33m"
#define COLOR_BLUE    "\033[34m"
#define COLOR_CYAN    "\033[36m"
#define COLOR_RESET   "\033[0m"

// 日志级别与颜色映射
#define COLOR_ERROR   COLOR_RED
#define COLOR_WARN    COLOR_YELLOW
#define COLOR_INFO    COLOR_GREEN
#define COLOR_DEBUG   COLOR_CYAN

log_write() 函数里,根据级别选择对应的颜色:

void log_write(int level, const char *file, int line,
               const char *func, const char *fmt, ...) {
    char timestamp[32];
    get_timestamp(timestamp, sizeof(timestamp));

    const char *color = "";
    const char *level_str = "";

    switch (level) {
        case LOG_LEVEL_ERROR:
            color = COLOR_ERROR;
            level_str = "ERROR";
            break;
        case LOG_LEVEL_WARN:
            color = COLOR_WARN;
            level_str = "WARN";
            break;
        case LOG_LEVEL_INFO:
            color = COLOR_INFO;
            level_str = "INFO";
            break;
        case LOG_LEVEL_DEBUG:
            color = COLOR_DEBUG;
            level_str = "DEBUG";
            break;
    }

    // 输出带颜色的日志
    printf("%s[%s] [%s] %s:%d %s() - ", color, timestamp,
           level_str, file, line, func);

    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);

    printf("%s\n", COLOR_RESET);
}

这样打印出来的日志,在终端里就是彩色的。错误信息红色,警告黄色,信息绿色,调试青色。一眼扫过去,哪里有问题清清楚楚。

核心要点:颜色只是辅助,不要为了好看而牺牲可读性。我建议只对日志级别着色,正文保持默认颜色。全篇花花绿绿反而看不清。

5.5 完整的日志格式示例

把上面所有内容整合起来,一条完整的日志长这样:

[2025-03-21 14:30:45.123] [ERROR] main.c:42 uart_send() - 发送超时,重试次数: 3

如果加上颜色,在终端里显示为:

红色 [2025-03-21 14:30:45.123] [ERROR] main.c:42 uart_send() - 发送超时,重试次数: 3

我曾经在一个项目中,因为日志格式不统一,有人用 printf 直接打印,有人用 fprintf(stderr, ...),还有人自己拼字符串。结果出问题后,日志文件里格式乱七八糟,根本没法用脚本分析。后来我统一了日志格式,所有模块都用同一个宏,问题一下子就清晰了。

5.6 知识体系图

下面这张图展示了日志格式定制的核心逻辑:

日志格式定制核心要素 日志格式 时间戳 日志级别 文件名 行号 函数名 宏 + __FILE__/__LINE__/__func__ gettimeofday() + strftime() ANSI \033[31m 颜色码 [时间] [级别] 文件:行 函数() - 消息

5.7 避坑指南

最后分享几个我踩过的坑:

  • 颜色在日志文件中没用:ANSI 颜色码在终端里有效,但如果把日志重定向到文件,颜色码会变成乱码。我建议在写入文件时去掉颜色,只保留纯文本。
  • 时间精度别太高:嵌入式系统里,gettimeofday() 的精度受系统时钟影响。如果系统时钟不准,毫秒级时间反而会误导人。
  • 函数名别太长:有些 C++ 项目里函数名带命名空间,一长串看着头疼。我习惯只取函数名本身,不包含类名和命名空间。
  • 行号会变:代码改了,行号就变了。别把行号当唯一标识,它只是辅助定位。

嗯,日志格式定制就讲到这里。记住一句话:格式统一比格式漂亮更重要。团队里所有人都用同一套格式,日志才能发挥最大价值。

公众号:蓝海资料掘金营,微信deep3321