日志系统设计:分级日志、时间戳、文件滚动、异步写入
日志系统这东西,说简单也简单,说复杂能写出一本书来。我早年做嵌入式项目时,一开始觉得日志嘛,printf 一下不就完了?结果产品上线后,客户报了个偶发死机的问题,我翻遍串口输出,全是乱糟糟的调试信息,根本定位不到问题。从那以后,我彻底明白了——日志不是随便打几个字,它是一门系统工程。
今天咱们就聊聊,怎么设计一个真正能用的嵌入式日志系统。核心就四个点:分级日志、时间戳、文件滚动、异步写入。一个一个来拆解。
1. 分级日志:别让信息淹死你
你想想看,如果所有日志都混在一起,调试信息、错误信息、普通运行状态全往一个地方堆,那跟垃圾场有什么区别?
我个人习惯把日志分成这么几级:
| 级别 | 宏定义 | 说明 |
|---|---|---|
| ERROR | LOG_LEVEL_ERROR | 致命错误,必须处理 |
| WARN | LOG_LEVEL_WARN | 警告,不影响运行但需关注 |
| INFO | LOG_LEVEL_INFO | 正常状态信息 |
| DEBUG | LOG_LEVEL_DEBUG | 调试用,发布时关闭 |
| TRACE | LOG_LEVEL_TRACE | 最细粒度,函数调用跟踪 |
代码里怎么用?我一般这样定义宏:
#define LOG_ERROR(fmt, ...) log_output(LOG_LEVEL_ERROR, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) log_output(LOG_LEVEL_WARN, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) log_output(LOG_LEVEL_INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) log_output(LOG_LEVEL_DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_TRACE(fmt, ...) log_output(LOG_LEVEL_TRACE, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
嗯,这里要注意:__FILE__ 和 __LINE__ 是编译器内置宏,能帮你快速定位日志来源。我在项目中遇到过,有人嫌麻烦没加这两个信息,结果出问题后满世界找代码,那叫一个痛苦。
2. 时间戳:精确到毫秒
日志没有时间戳,就像历史书没有年份。你只能知道发生了什么,却不知道什么时候发生的。对于排查时序相关的问题,这简直是灾难。
我建议时间戳格式为:2025-03-15 14:23:45.678。为什么精确到毫秒?因为嵌入式系统里很多问题发生在毫秒级,比如中断抢占、任务切换。
获取时间戳的代码大致这样:
void get_timestamp(char *buf, size_t len) {
struct timeval tv;
struct tm *tm_info;
gettimeofday(&tv, NULL);
tm_info = localtime(&tv.tv_sec);
snprintf(buf, len, "%04d-%02d-%02d %02d:%02d:%02d.%03ld",
tm_info->tm_year + 1900,
tm_info->tm_mon + 1,
tm_info->tm_mday,
tm_info->tm_hour,
tm_info->tm_min,
tm_info->tm_sec,
tv.tv_usec / 1000);
}
我曾经在一个项目中,因为时间戳只精确到秒,结果两个事件的时间戳一模一样,根本分不清先后顺序。后来改成毫秒级,问题迎刃而解。
3. 文件滚动:别让日志撑爆存储
嵌入式设备的存储空间通常有限。如果不做文件滚动,日志文件会一直增长,直到把 Flash 或 SD 卡写满。到时候系统可能直接挂掉。
文件滚动的核心思路是:保留最近 N 个日志文件,旧的自动删除。常见的策略有两种:
- 按大小滚动: 单个日志文件达到指定大小(比如 1MB)后,自动切换到下一个文件。比如 log_0.txt、log_1.txt……最多保留 5 个。
- 按时间滚动: 每天或每小时生成一个新文件。比如 log_20250315.txt、log_20250316.txt……保留最近 7 天。
我个人更倾向于按大小滚动,因为嵌入式系统的存储空间是固定的,按大小滚动更容易控制总容量。代码实现大致如下:
#define MAX_LOG_FILES 5
#define MAX_LOG_SIZE (1024 * 1024) // 1MB
void log_rotate(void) {
// 检查当前文件大小
long size = get_file_size(current_log_file);
if (size < MAX_LOG_SIZE) return;
// 删除最旧的文件
char oldest[64];
snprintf(oldest, sizeof(oldest), "log_%d.txt",
(current_index - MAX_LOG_FILES + 1) % MAX_LOG_FILES);
remove(oldest);
// 切换到下一个文件
current_index = (current_index + 1) % MAX_LOG_FILES;
snprintf(current_log_file, sizeof(current_log_file),
"log_%d.txt", current_index);
// 打开新文件,准备写入
}
4. 异步写入:别让日志拖慢系统
这是最容易被忽视的一点。很多人写日志时直接调用 fwrite 或 fprintf,结果日志一多,系统响应就变慢。为什么?因为写文件是 I/O 操作,耗时可能是微秒级甚至毫秒级。在实时性要求高的场景下,这绝对不能接受。
异步写入的思路是:日志先写入内存缓冲区,然后由专门的线程或任务负责写入文件。这样,业务代码只管往缓冲区里塞数据,不用等文件写完。
我常用的实现方式:
#define LOG_BUF_SIZE 4096
static char log_buffer[LOG_BUF_SIZE];
static int buf_pos = 0;
void log_output(int level, const char *file, int line, const char *fmt, ...) {
// 格式化日志内容
char msg[256];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
// 写入缓冲区(注意加锁)
pthread_mutex_lock(&log_mutex);
int len = snprintf(log_buffer + buf_pos, LOG_BUF_SIZE - buf_pos,
"[%s] [%s] %s:%d - %s\n",
get_level_str(level), get_timestamp_str(),
file, line, msg);
buf_pos += len;
// 如果缓冲区快满了,触发写入
if (buf_pos > LOG_BUF_SIZE - 256) {
pthread_cond_signal(&log_cond);
}
pthread_mutex_unlock(&log_mutex);
}
// 专门的写入线程
void *log_writer_thread(void *arg) {
while (1) {
pthread_mutex_lock(&log_mutex);
while (buf_pos == 0) {
pthread_cond_wait(&log_cond, &log_mutex);
}
// 写入文件
fwrite(log_buffer, 1, buf_pos, log_file);
fflush(log_file);
buf_pos = 0;
pthread_mutex_unlock(&log_mutex);
}
return NULL;
}
你看,业务代码和文件写入完全解耦了。即使文件写入偶尔卡一下,也不会影响主流程。
整体架构图
说了这么多,咱们用一张图把整个日志系统的结构串起来:
从这张图可以看得很清楚:业务代码只管调用宏,核心层负责过滤和格式化,异步线程负责写入,文件滚动层负责管理存储。每一层各司其职,互不干扰。
避坑指南
最后,分享几个我踩过的坑:
- 日志缓冲区太小: 我曾经设了个 256 字节的缓冲区,结果日志一多就溢出,导致数据错乱。建议至少 4KB,如果内存允许,8KB 更好。
- 忘记加锁: 多线程环境下,日志缓冲区是共享资源。不加锁的话,两个线程同时写入,数据就乱了。我吃过这个亏,调试了整整两天才发现是日志系统本身的问题。
- fprintf 的陷阱: fprintf 内部会做格式化,性能比 fwrite 差很多。异步写入时,建议先用 snprintf 格式化好,再用 fwrite 写入文件。
- 日志级别设置不当: 发布版本时,DEBUG 和 TRACE 级别的日志一定要关掉。否则不仅浪费存储,还会拖慢系统。我见过一个产品,发布时忘了关 DEBUG,结果日志文件一天就写满了 128MB 的 Flash。
好了,日志系统设计就聊到这里。记住一句话:好的日志系统,平时感觉不到它的存在,出问题时它就是你最好的朋友。