17、日志压缩与归档:gzip压缩日志,按日期归档,自动清理旧日志

日志文件这东西,跑着跑着就大了。我见过最夸张的一次,一个嵌入式设备跑了三个月没重启,日志文件撑到了2个G。别说分析了,连打开都费劲。所以,日志压缩与归档,不是锦上添花,是刚需。

说白了,我们要做三件事:压缩(把文本变小)、归档(按日期分好类)、清理(把太老的删掉)。这三件事串起来,就是一个自动化的日志管家。

为什么非压缩不可?

嵌入式设备的存储空间,你懂的。Flash 就那么大,SD 卡也有限。日志如果不压缩,几天就能把存储吃光。我有个项目,客户要求保留30天日志,但设备只有256MB的Flash。不压缩?一天都撑不住。

gzip 压缩率很高。纯文本日志,通常能压到原来的10%~20%。也就是说,1MB的日志,压完可能就100多KB。这差距,你想想看。

gzip 压缩:C语言里怎么搞?

C语言里做gzip压缩,最常用的库是 zlib。它提供了 gzopengzwritegzclose 这些接口,用起来跟操作普通文件差不多。

我个人习惯,写日志的时候直接写进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",每次都会覆盖文件。我刚开始用的时候踩过这个坑,日志只保留了最后一条,排查了半天。

小提示:zlib 库在大多数嵌入式Linux环境中都预装了。如果是裸机或RTOS环境,可能需要自己交叉编译。记得在链接时加上 -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);
}
注意:strptime 在某些嵌入式C库(如newlib)中可能不可用。如果遇到这个问题,可以自己写一个简单的日期解析函数,或者用 sscanf 手动拆解年、月、日。

整体架构:一张图说清楚

我把整个流程画成了SVG图。你看一眼就明白了。

日志压缩与归档流程 生成日志消息 获取当前日期 gzip压缩写入文件 清理超过N天的旧日志 关键点 • 每天一个压缩文件 • 文件名含日期: log_YYYY-MM-DD.gz • 保留天数可配置 • 写日志时自动触发清理 • 使用zlib库进行gzip • 支持断电恢复 —— 我踩过的坑 —— 不要用"wb"模式! 会覆盖历史日志。

避坑指南:我曾经...

我曾经在一个项目中,直接用了 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")

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