lumberjack 的 maxage 仅控制旧备份文件清理而非每日轮转,因它只检查 modtime() 删除过期文件,不触发当日日志文件重命名;按大小轮转是唯一开箱即用方案,而按天轮转需手动拼日期、加锁切换并处理时区、并发、权限等细节。

直接用 logrus.SetOutput(&lumberjack.Logger{}) 就能实现按大小轮转,但“定期轮转”——比如每天一个文件——lumberjack 本身不支持,必须自己控制文件名和切换时机。
为什么 lumberjack 的 MaxAge 不等于每天切一次
MaxAge 是清理策略,不是切割触发器。它只在每次写入前检查旧备份文件的 ModTime(),决定是否删除(例如删掉 7 天前的 app.log.1),但不会导致当天的 app.log 在零点被重命名或新建。
- 常见错误现象:
app.log从 8 月 10 日下午写到 8 月 11 日上午,文件名没变、内容跨天 - 设了
MaxAge: 1,结果第二天日志还在往老文件里写,只是旧备份被删了 -
LocalTime: true必须显式开启,否则ModTime()是 UTC 时间,和本地时区对不上,清理可能误判 - 它不解析日志内容里的日期字段,只看文件元信息(
os.FileInfo.ModTime())
按大小轮转:最稳的配置方式
这是 lumberjack 原生支持、线程安全、无需额外逻辑的模式。关键在于参数含义和注入方式不能错。
- 必须直接传
&lumberjack.Logger{}给logrus.SetOutput(),不能包装成io.Writer或中间层,否则轮转失效 -
MaxSize单位是字节,不是 MB;写100 * 1024 * 1024,别写100或"100MB" -
Mode: 0644必须显式设置,否则新日志文件权限为0600,运维查日志时读不到 -
LocalTime: true要开,否则备份文件名里的日期戳是 UTC,排查时容易看错 - Windows 下注意:
os.Rename若目标文件被其他进程占用(如记事本打开),轮转会静默失败,无报错
真要“每天一个文件”,得手动拼日期 + 加锁切换
核心动作是:每次写入前比对当前日期字符串,变了就关旧文件、开新文件,整个过程必须加锁且原子。
- 封装一个结构体,缓存当前
Filename(如/var/log/app/app-2026-08-11.log)和today字符串("2026-08-11") - 每次
Write(p []byte)前调time.Now().Format("2006-01-02")对比,不一致则先Close()再重建*lumberjack.Logger - 必须用
sync.Mutex包住整个判断 + 关闭 + 打开流程,否则并发写时可能往已关闭的文件句柄写,触发write: bad file descriptor - 不要用
time.AfterFunc或 goroutine 定时轮询——进程重启后定时器丢失,且零点毫秒级漂移会导致漏切 - 如果用 Gin,建议把该逻辑封装进自定义
io.WriteCloser,再传给logrus.SetOutput(),而不是塞进中间件里反复判断
Gin 中双输出(终端 + 文件)的典型陷阱
很多人想同时输出到 console 和轮转文件,直接用 io.MultiWriter(os.Stdout, &lumberjack.Logger{}) 看似简单,但有隐患。
-
lumberjack.Logger.Close()只关文件,不影响os.Stdout;但若程序退出前没调Close(),最后几条日志可能因缓冲未刷而丢失 - 更稳妥的做法是:用
logrus.AddHook()分离输出,或用io.MultiWriter但确保defer logger.Close()在 main 函数末尾执行 - 如果日志量大(如每秒百条),
io.MultiWriter会阻塞主线程,因为所有写操作都同步;此时应考虑异步 hook 或 zap 的core模式 - Gin 的
c.Request.RemoteAddr等字段在中间件中可用,但别在Write()调用链里再取,避免竞态或 panic
按大小轮转是唯一开箱即用、无需额外维护的方案;按天轮转看着直观,但实际要处理时区、进程生命周期、并发安全、权限、路径一致性这些细节,稍一疏忽就会丢日志或卡死。真正上线前,务必用高频率写入压测切换逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











