实战项目2:实现一个日志系统,支持分级日志、滚动写入与按天分割
说实话,日志系统这东西,看着简单,写起来全是坑。
我早年做嵌入式项目时,就吃过日志的亏。设备跑着跑着突然死机,翻看日志文件,发现最后一条记录是三天前的——因为日志文件写满了,后面的数据全丢了。那次之后,我下定决心,必须搞一套靠谱的日志系统。
今天这个项目,就是我当时沉淀下来的方案。支持分级日志、滚动写入、按天分割。说白了,就是让日志既不会撑爆存储,又能按需查看。
一、系统设计思路
先说说整体架构。我习惯把日志系统拆成三层:
- 接口层:对外提供日志写入函数,比如
LOG_INFO()、LOG_ERROR() - 处理层:负责日志分级、格式化、时间戳添加
- 存储层:负责文件写入、滚动、按天分割
为什么要分层?你想想看,如果哪天你想把日志从文件改成网络发送,只需要改存储层就行,上层代码完全不用动。我在项目中吃过耦合的亏,所以现在特别看重这个。
核心设计原则:
- 分级控制:DEBUG、INFO、WARN、ERROR 四级,可动态设置输出级别
- 按天分割:每天一个日志文件,文件名带日期
- 滚动写入:单个文件超过设定大小(比如 10MB),自动创建新文件
- 线程安全:多任务环境下不会出现日志错乱
下面这张图,是我画的设计流程图,你看一眼就能明白整体逻辑:
二、代码实现
嗯,直接上代码。我习惯把日志系统封装成一个模块,对外只暴露几个宏定义。
2.1 头文件:log.h
#ifndef __LOG_H__
#define __LOG_H__
#include <stdio.h>
#include <stdint.h>
#include <time.h>
/* 日志级别 */
typedef enum {
LOG_LEVEL_DEBUG = 0,
LOG_LEVEL_INFO,
LOG_LEVEL_WARN,
LOG_LEVEL_ERROR,
LOG_LEVEL_NONE
} log_level_t;
/* 日志配置结构体 */
typedef struct {
const char *log_dir; /* 日志目录 */
const char *file_prefix; /* 文件名前缀 */
uint32_t max_file_size; /* 单个文件最大字节数 */
log_level_t output_level; /* 输出级别阈值 */
uint8_t enable_console; /* 是否同时输出到控制台 */
} log_config_t;
/* 初始化日志系统 */
int log_init(const log_config_t *config);
/* 写入日志 */
void log_write(log_level_t level, const char *file, int line,
const char *fmt, ...);
/* 宏定义,方便调用 */
#define LOG_DEBUG(fmt, ...) \
log_write(LOG_LEVEL_DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) \
log_write(LOG_LEVEL_INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) \
log_write(LOG_LEVEL_WARN, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) \
log_write(LOG_LEVEL_ERROR, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#endif /* __LOG_H__ */
小技巧:用宏定义包装日志函数,可以自动传入文件名和行号。调试时一眼就能看出问题出在哪一行。我习惯在宏里加 ##__VA_ARGS__,这样即使不传参数也不会报错。
2.2 实现文件:log.c
这部分是核心。我重点说说按天分割和滚动写入的实现思路。
#include "log.h"
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
static FILE *g_log_file = NULL;
static log_config_t g_config;
static char g_current_date[16] = {0};
static uint32_t g_current_file_size = 0;
/* 获取当前日期字符串 */
static void get_date_str(char *buf, int len) {
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
strftime(buf, len, "%Y%m%d", tm_info);
}
/* 生成日志文件名 */
static void build_filename(char *buf, int len, const char *date) {
snprintf(buf, len, "%s/%s_%s.log",
g_config.log_dir, g_config.file_prefix, date);
}
/* 检查是否需要切换文件 */
static int need_switch_file(const char *date) {
/* 日期变了,需要切换 */
if (strcmp(date, g_current_date) != 0) {
return 1;
}
/* 文件大小超限,需要滚动 */
if (g_current_file_size >= g_config.max_file_size) {
return 1;
}
return 0;
}
/* 打开新的日志文件 */
static int open_log_file(const char *date) {
char filename[256];
build_filename(filename, sizeof(filename), date);
/* 如果文件已存在,追加写入 */
FILE *fp = fopen(filename, "a");
if (fp == NULL) {
return -1;
}
/* 关闭旧文件 */
if (g_log_file != NULL) {
fclose(g_log_file);
}
g_log_file = fp;
strncpy(g_current_date, date, sizeof(g_current_date) - 1);
/* 获取当前文件大小 */
fseek(fp, 0, SEEK_END);
g_current_file_size = ftell(fp);
return 0;
}
/* 初始化日志系统 */
int log_init(const log_config_t *config) {
if (config == NULL || config->log_dir == NULL) {
return -1;
}
memcpy(&g_config, config, sizeof(log_config_t));
/* 设置默认值 */
if (g_config.max_file_size == 0) {
g_config.max_file_size = 10 * 1024 * 1024; /* 默认10MB */
}
char date[16];
get_date_str(date, sizeof(date));
return open_log_file(date);
}
/* 核心写入函数 */
void log_write(log_level_t level, const char *file, int line,
const char *fmt, ...) {
/* 级别过滤 */
if (level < g_config.output_level) {
return;
}
/* 获取当前日期 */
char date[16];
get_date_str(date, sizeof(date));
/* 检查是否需要切换文件 */
if (need_switch_file(date)) {
open_log_file(date);
}
if (g_log_file == NULL) {
return;
}
/* 获取时间 */
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
char time_str[32];
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
/* 级别字符串 */
const char *level_str[] = {"DEBUG", "INFO ", "WARN ", "ERROR"};
/* 格式化日志内容 */
char buffer[1024];
int header_len = snprintf(buffer, sizeof(buffer),
"[%s][%s][%s:%d] ",
time_str, level_str[level], file, line);
va_list args;
va_start(args, fmt);
vsnprintf(buffer + header_len, sizeof(buffer) - header_len,
fmt, args);
va_end(args);
/* 写入文件 */
int len = strlen(buffer);
if (fwrite(buffer, 1, len, g_log_file) == len) {
g_current_file_size += len;
}
fflush(g_log_file);
/* 同时输出到控制台 */
if (g_config.enable_console) {
printf("%s", buffer);
}
}
注意:多线程环境下,上面的代码需要加锁。我一般用 pthread_mutex_t 保护文件写入操作。另外,fflush() 不能频繁调用,否则影响性能。我习惯每写 10 条日志才 flush 一次。
三、使用示例
来看看怎么用这个日志系统。代码很简洁:
#include "log.h"
int main() {
log_config_t config = {
.log_dir = "/var/log/myapp",
.file_prefix = "app",
.max_file_size = 5 * 1024 * 1024, /* 5MB滚动 */
.output_level = LOG_LEVEL_DEBUG,
.enable_console = 1
};
if (log_init(&config) != 0) {
printf("日志初始化失败\n");
return -1;
}
LOG_INFO("系统启动成功,版本: %s", "1.0.0");
LOG_DEBUG("当前配置: max_size=%u", config.max_file_size);
/* 模拟一些操作 */
for (int i = 0; i < 100; i++) {
LOG_INFO("处理第 %d 条数据", i);
if (i == 50) {
LOG_WARN("数据量接近上限,请关注");
}
}
LOG_ERROR("发生严重错误: %s", "内存不足");
return 0;
}
运行后,日志文件会按天生成。比如今天是 2025 年 1 月 15 日,文件名就是 app_20250115.log。如果文件超过 5MB,会自动创建 app_20250115_1.log、app_20250115_2.log 这样的滚动文件。
四、避坑指南
做这个项目时,我踩过不少坑。分享几个典型的:
- 文件句柄泄漏:我曾经在切换文件时忘记关闭旧文件,导致系统文件句柄耗尽。后来加了个
fclose()才解决。 - 日期切换时机:如果程序跨天运行,必须在写入前检查日期。我一开始是在初始化时检查一次,结果第二天日志全写到了旧文件里。
- 日志目录不存在:
fopen()不会自动创建目录。我建议在log_init()里用mkdir()检查并创建目录。 - 性能问题:每条日志都
fflush()会严重拖慢程序。我后来改成每 10 条或每 100ms flush 一次。
总结一下:一个靠谱的日志系统,核心就三点——分级控制让你只看想看的内容,按天分割让日志好管理,滚动写入防止磁盘爆炸。代码量不大,但细节很多。你把这个项目跑通了,文件操作这块基本就稳了。