17、日志压缩与归档:gzip压缩日志,按日期归档,自动清理旧日志
日志文件这东西,跑着跑着就大了。我见过最夸张的一次,一个嵌入式设备跑了三个月没重启,日志文件撑到了2个G。别说分析了,连打开都费劲。所以,日志压缩与归档,不是锦上添花,是刚需。
说白了,我们要做三件事:压缩(把文本变小)、归档(按日期分好类)、清理(把太老的删掉)。这三件事串起来,就是一个自动化的日志管家。
为什么非压缩不可?
嵌入式设备的存储空间,你懂的。Flash 就那么大,SD 卡也有限。日志如果不压缩,几天就能把存储吃光。我有个项目,客户要求保留30天日志,但设备只有256MB的Flash。不压缩?一天都撑不住。
gzip 压缩率很高。纯文本日志,通常能压到原来的10%~20%。也就是说,1MB的日志,压完可能就100多KB。这差距,你想想看。
gzip 压缩:C语言里怎么搞?
C语言里做gzip压缩,最常用的库是 zlib。它提供了 gzopen、gzwrite、gzclose 这些接口,用起来跟操作普通文件差不多。
我个人习惯,写日志的时候直接写进gzip文件。这样省去了「先写文本、再压缩」的中间步骤,既省空间又省时间。
#include <zlib.h>
void write_compressed_log(const char *filename, const char *message) {
gzFile gz = gzopen(filename, "ab"); // 追加模式,二进制
if (gz == NULL) {
perror("gzopen failed");
return;
}
gzwrite(gz, message, strlen(message));
gzclose(gz);
}
嗯,这里要注意:gzopen 的第二个参数,如果是 "ab",表示追加写入。如果你用 "wb",每次都会覆盖文件。我刚开始用的时候踩过这个坑,日志只保留了最后一条,排查了半天。
-lz。
按日期归档:文件名里藏时间
归档的核心思路很简单:每天一个文件。文件名里带上日期,比如 log_2025-01-17.gz。这样查找、清理都方便。
获取当前日期,C语言里用 time() 和 localtime() 就行。
#include <time.h>
void get_date_str(char *buf, size_t size) {
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
strftime(buf, size, "%Y-%m-%d", tm_info);
}
然后组合文件名:
char filename[128];
char date[16];
get_date_str(date, sizeof(date));
snprintf(filename, sizeof(filename), "/var/log/log_%s.gz", date);
这样,每天的日志都会写进当天的压缩文件里。跨天了?自动换文件。完美。
自动清理旧日志:别让存储爆了
归档做得好,但如果不清理,存储迟早会满。我建议的策略是:保留最近N天,删除更早的。
实现方式有两种:
- 主动扫描法:每次写日志前,扫描日志目录,删除超过保留天数的文件。
- 定时任务法:用cron或定时器,每天凌晨执行一次清理脚本。
我个人更推荐主动扫描法。为什么?因为嵌入式设备可能随时断电,定时任务不一定能准时执行。而主动扫描法,只要写日志就会触发,可靠性更高。
扫描目录、解析文件名中的日期、比较天数差,这些操作在C语言里都能做。不过要注意,嵌入式设备上可能没有 scandir,得用 opendir + readdir 自己遍历。
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
void clean_old_logs(const char *dir, int keep_days) {
DIR *d = opendir(dir);
if (d == NULL) return;
struct dirent *entry;
time_t now = time(NULL);
while ((entry = readdir(d)) != NULL) {
// 只处理 .gz 文件
if (strstr(entry->d_name, ".gz") == NULL) continue;
// 从文件名中解析日期(假设格式 log_YYYY-MM-DD.gz)
char date_str[11];
if (sscanf(entry->d_name, "log_%10s", date_str) != 1) continue;
// 将日期字符串转为时间戳
struct tm tm = {0};
if (strptime(date_str, "%Y-%m-%d", &tm) == NULL) continue;
time_t file_time = mktime(&tm);
// 计算天数差
double diff = difftime(now, file_time) / (60 * 60 * 24);
if (diff > keep_days) {
char full_path[256];
snprintf(full_path, sizeof(full_path), "%s/%s", dir, entry->d_name);
remove(full_path);
printf("Deleted old log: %s\n", full_path);
}
}
closedir(d);
}
整体架构:一张图说清楚
我把整个流程画成了SVG图。你看一眼就明白了。
避坑指南:我曾经...
我曾经在一个项目中,直接用了 system("gzip log.txt") 来压缩日志。当时觉得省事,结果出大事了。那个设备是裁剪过的Linux,没有gzip命令。程序跑起来直接报错,日志没压缩成,反而把原始文件搞丢了。
所以,永远不要在嵌入式设备上依赖外部命令。用zlib库,静态链接进去,稳得很。
还有一个坑:文件描述符泄漏。每次写日志都 gzopen,但忘记 gzclose。跑几天后,系统报 "Too many open files"。嗯,这个我犯过不止一次。后来我写了个封装,在函数末尾统一关闭,才彻底解决。
总结一下
日志压缩与归档,说白了就是三招:压缩、归档、清理。用zlib做gzip压缩,用日期做文件名归档,用扫描删除做自动清理。这三招组合起来,你的日志系统就能自动运转,再也不用担心存储爆满。
我个人习惯,把保留天数做成可配置的宏或者配置文件参数。这样不同项目可以灵活调整。有的项目需要保留7天,有的需要30天,改一个数字就行,不用动代码。
- 使用zlib库的gzopen/gzwrite/gzclose进行gzip压缩
- 文件名格式:log_YYYY-MM-DD.gz,按日期归档
- 写日志时主动扫描并删除超过保留天数的旧文件
- 保留天数建议做成可配置参数
- 注意文件描述符泄漏和模式选择(用"ab"不要用"wb")