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);,剩下的信息宏会自动填充。说白了,这就是用宏来偷懒,但偷得漂亮。
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 没有这个函数,可以用系统滴答定时器自己算。
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 知识体系图
下面这张图展示了日志格式定制的核心逻辑:
5.7 避坑指南
最后分享几个我踩过的坑:
- 颜色在日志文件中没用:ANSI 颜色码在终端里有效,但如果把日志重定向到文件,颜色码会变成乱码。我建议在写入文件时去掉颜色,只保留纯文本。
- 时间精度别太高:嵌入式系统里,
gettimeofday()的精度受系统时钟影响。如果系统时钟不准,毫秒级时间反而会误导人。 - 函数名别太长:有些 C++ 项目里函数名带命名空间,一长串看着头疼。我习惯只取函数名本身,不包含类名和命名空间。
- 行号会变:代码改了,行号就变了。别把行号当唯一标识,它只是辅助定位。
嗯,日志格式定制就讲到这里。记住一句话:格式统一比格式漂亮更重要。团队里所有人都用同一套格式,日志才能发挥最大价值。