lumberjack轮转不触发的根本原因是仅依赖单个文件大小(maxsize,单位mb)触发,而非时间;若日志量小则长期不轮转,maxage/maxbackups仅影响后续清理。

直接用 gin.LoggerWithWriter 配合 lumberjack.Logger 就能实现自动轮转,但多数人卡在轮转不生效、日志写不进文件、或备份文件没压缩这几个点上。
为什么 lumberjack 轮转不触发?
根本原因是 lumberjack.Logger 的轮转只依赖单个文件大小(MaxSize),不看时间或日期。它不会“每天生成一个新文件”,而是等当前日志文件达到设定阈值(比如 100MB)才切分。
-
MaxSize单位是 MB,不是字节 —— 写成100 * 1024 * 1024或104857600都会失效,必须传整数如100 - 轮转只发生在写入时判断:如果日志量小,可能跑一周都不会触发一次轮转
-
MaxAge和MaxBackups是清理策略,不影响轮转触发时机;它们只在下一次写入时检查旧文件是否过期/超量 - 别指望它像 logrotate 那样按天切割 —— 要按天命名,得自己封装一层或换 zap + file-rotatelogs
gin.LoggerWithWriter + lumberjack 最简可用写法
常见错误是直接传 &lumberjack.Logger{...} 给 gin.LoggerWithWriter(),但没注意它底层调用的是 io.Writer 接口,而 lumberjack 实例本身实现了该接口 —— 所以可以直接传指针,但必须确保字段初始化完整。
- 必须显式设置
Filename,否则日志写到 stdout 或 panic -
LocalTime: true建议打开,避免时区错乱导致MaxAge判定异常 -
Compress: true仅对已归档的旧文件生效(即 .gz 后缀只出现在 backup 文件上),当前活跃日志仍是明文 - 示例:
logger := &lumberjack.Logger{ Filename: "logs/access.log", MaxSize: 100, // MB MaxBackups: 7, MaxAge: 30, // 天 LocalTime: true, Compress: true, } r.Use(gin.LoggerWithWriter(logger))
多个日志类型要分开实例,不能共用同一个 lumberjack.Logger
lumberjack.Logger 内部用 mutex 保护单个文件句柄,如果把同一个实例同时塞给 gin.LoggerWithWriter 和 gin.RecoveryWithWriter,会出现写冲突、日志丢失、甚至 panic。
- 访问日志和错误日志必须用两个独立实例:
accessLogger和errorLogger - 路径不能相同,哪怕只是后缀不同(如
access.log和access.log.err)也不行 —— lumberjack 会尝试清理所有匹配Filename前缀的文件,造成误删 - 如果要用同一目录下不同前缀,务必确保
Filename完全独立,例如:"logs/access.log"和"logs/error.log"
生产环境绕不开的权限与路径问题
Go 进程启动时工作目录不固定,Filename: "logs/access.log" 很可能写到根目录或 /tmp 下,或者因权限不足直接静默失败。
- 务必用绝对路径,比如
/var/log/myapp/access.log,并在启动前mkdir -p /var/log/myapp+chown myapp:myapp /var/log/myapp - Windows 下路径分隔符要用
/或filepath.Join,硬写\会导致编译报错或运行时路径错误 - 容器部署时,宿主机挂载目录权限常为 root,而应用用户非 root —— 必须提前
chmod 755目录并确认 UID 匹配 - 轮转后旧文件权限默认继承原文件,但压缩文件(.gz)可能因 umask 导致不可读,建议显式设置
Mode: 0644(lumberjack v2.3+ 支持)
轮转本身很简单,真正麻烦的是路径、权限、多实例隔离和清理逻辑 —— 这些地方一出错,日志就“看起来在写,其实没落地”。











