log4cplus和spdlog滚动需精准配置:log4cplus需显式设schedule及MaximumFileSize+MaxBackupIndex;spdlog中rotating_logger_mt与daily_logger_mt不可混用,多条件滚动须手动封装;跨平台滚动须close-rename-reopen并加锁;时区安全应基于UTC或缓存时间戳判断;高并发下需异步解耦滚动逻辑。

log4cplus 或 spdlog 能直接支持滚动,但得配对参数
多数人卡在“启用了滚动却没生效”,本质是配置项和实际行为不匹配。比如 log4cplus 的 RollingFileAppender 默认不启用日期滚动,必须显式设 schedule=HOURLY 或 DAILY;而大小滚动依赖 MaximumFileSize 和 MaxBackupIndex 两个值同时有效——漏掉 MaxBackupIndex,日志写满后就直接覆盖原文件,不会重命名归档。
用 spdlog 更直观些:spdlog::rotating_logger_mt 控制按大小滚动,spdlog::daily_logger_mt 控制按日期滚动,两者不能混用同一个 logger 实例。常见错误是试图在一个 logger 上既设最大尺寸又设每天零点切分,结果只有后者生效(因为 daily logger 内部忽略 size 参数)。
-
spdlog::rotating_logger_mt("name", "app.log", 10485760, 5)—— 单文件上限 10MB,最多保留 5 个历史文件 -
spdlog::daily_logger_mt("name", "app.log", 2, 30)—— 每天 2:30 切分(注意:不是系统时间零点,是构造时指定的 hour/min) - 若需“每天切 + 单个文件再按大小滚”,得自己封装一层:每日新建一个
rotating_logger_mt,文件名带日期前缀,例如app_20240520.log
手动实现滚动要绕开 rename 被占用的问题
Windows 下直接 std::rename("app.log", "app.log.1") 经常失败,报错 Access is denied,因为日志文件仍被 std::ofstream 打开着。正确做法是先 flush()、再 close() 当前流,rename 完再用新路径 reopen。Linux 虽支持已打开文件 rename,但旧 fd 仍写原 inode,会导致磁盘空间不释放——所以跨平台统一处理必须 close-rename-reopen。
另一个坑是并发写入:多个线程共用一个 logger 时,滚动操作(close+rename+reopen)必须加锁,且锁粒度要覆盖整个滚动流程,不能只锁 rename。否则可能 A 线程刚 close,B 线程还在 write,就会 crash 或丢日志。
- 滚动前调用
stream.flush(),确保缓冲区内容落盘 - 务必检查
stream.is_open()再 close,避免重复 close 异常 - rename 后立即尝试 open 新文件,失败则回退并记录错误到 stderr(此时不能再写日志文件)
- 推荐用
std::fstream配合std::ios::app | std::ios::out模式,避免每次写都重定位
按日期滚动时要注意时区与午夜边界
很多实现用 localtime(&t) 取日期,结果在服务器切换时区或夏令时后,某天生成两个同名日志(如 app_20240310.log 和 app_20240310_1.log),或者跳过一天。根本原因是未固化判断逻辑——应该用 UTC 时间做切分基准,或至少缓存上一次切分的日期值,仅当当前日期 > 缓存日期时才触发滚动。
更稳妥的做法是:启动时计算下一个切分时间点(比如今天 00:00:00 加 24 小时),后续每条日志写入前检查系统时间是否 ≥ 该时间点。一旦命中,执行滚动并更新下个时间点。这样不受 localtime 调用时机、时区变更、NTP 校时跳变影响。
- 避免用
strftime(..., "%Y%m%d")动态拼接文件名作为唯一判断依据 - 缓存上一滚动时间戳(
time_t),比对用std::difftime(now, last_roll) >= 86400 - 如果要求严格按自然日(而非 24 小时周期),就取
tm_hour == 0 && tm_min == 0 && tm_sec == 0作为信号,但需容忍几秒延迟
大流量场景下滚动会阻塞主线程,得异步化
当日志量大(比如每秒万级 log line)、磁盘慢(如机械盘或网络存储),一次滚动可能耗时几十毫秒甚至上百毫秒。同步执行会拖慢业务线程。解决方案不是简单扔进 std::thread,而是把“写日志”和“滚动动作”解耦:业务线程只往无锁队列 push 日志消息,后台线程消费队列并负责判断是否滚动、写文件、rename 等所有 IO 操作。
这里的关键是滚动决策必须由后台线程独占判断。如果业务线程也参与判断(比如每写 1000 行检查一次 size),会出现竞态:两个线程同时发现要滚动,结果重复 rename 或文件被截断。
- 使用
moodycamel::ConcurrentQueue或boost::lockfree::queue做消息通道 - 滚动阈值(大小/时间)只由后台线程读取和更新,业务线程不感知
- 后台线程每次 flush 前检查是否需滚动,是则先 close+rename+reopen,再继续写
- 注意:异步模式下无法保证日志 100% 不丢,进程崩溃时队列中未消费消息会丢失
滚动看似只是改个文件名,但涉及文件系统语义、多线程安全、时序一致性、IO 性能权衡。最易被忽略的是“滚动操作本身不是原子的”,哪怕用 rename(),中间 close 和 reopen 之间仍有空窗——这期间如果有日志写入请求,必须排队或丢弃,得提前设计好 fallback 行为。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











