lumberjack.MaxAge ≠ 每天零点切分,因其仅按文件修改时间清理旧日志,不主动触发零点新建带日期文件;必须配合LocalTime: true、MaxSize: 0,并每日零点手动调用Rotate()(加锁串行化),才能实现app.log.2026-07-01式严格按日切分。

Go 微服务里想让日志按天切分(比如 app.log.2026-07-01),不能只靠 lumberjack 配个 MaxAge: 24 * time.Hour 就完事——它不会在零点新建文件,只会等下次写入时检查旧文件是否“满 24 小时”,导致跨天日志混在一个文件里。
为什么 lumberjack.MaxAge ≠ 每天零点切分
MaxAge 是清理策略,不是切割触发器。它只在每次写入前检查当前日志文件的 ModTime(),判断是否该归档(比如删掉 7 天前的 app.log.2026-06-24),但不会主动把正在写的 app.log 在 00:00:00 重命名为带当天日期的新文件。
- 常见错误现象:
app.log从 6 月 30 日 23:59 写到 7 月 1 日 00:05,文件名没变,内容跨日 - 设了
MaxAge: 24 * time.Hour,结果 7 月 1 日上午日志还在往老文件里写,只是 6 月 30 日前的备份被删了 -
LocalTime: false(默认)会导致文件名用 UTC 时间,北京时间 8 点才“认为”是新一天,彻底错位
必须手动调用 Rotate() + 定时检测新一天
真正实现严格按日切分,得自己起 goroutine,在每天 00:00:00 调用 lumberjack.Rotate()。注意这不是“让它自动轮转”,而是你主动告诉它:“现在换文件”。
- 用
time.Ticker每分钟检查一次最稳妥:避免时钟跳变、NTP 校正导致的毫秒级漂移误判 - 判断逻辑必须用
t.Day() != lastDay,而不是格式化字符串比较(比如t.Format("2006-01-02")),否则时区或格式误差会漏切 -
Rotate()不是原子操作,多 goroutine 并发调用会 panic;必须用sync.Mutex或 channel 串行化 - 首次启动时,如果已有同名日志文件,
lumberjack会读其ModTime()判断是否该切——确保系统时间准确,容器需挂载/etc/localtime
logrus/zap/slog 怎么接 lumberjack 才不丢日志
所有主流日志库都只认 io.Writer,lumberjack.Logger 实现了这个接口,但直接传进去还不够——高频写入下,lumberjack 内部锁不足以覆盖你的业务并发模型。
- logrus:别用
logger.Out = lw,改用logrus.SetOutput(lw),它内部有同步保护 - zap:必须用
zapcore.AddSync(lw)包一层,否则WriteSyncer可能绕过lumberjack的锁 - slog:用
slog.New(slog.NewTextHandler(lw, nil)),handler 本身不缓存,但lw必须是你已加好定时Rotate()的实例 - 千万别在 HTTP handler 里调
lw.Rotate()——可能卡住响应;也别用time.AfterFunc,进程重启后定时器丢失
MaxSize 设为 0 还是设很大?
想纯按天切,MaxSize 必须设为 0(禁用大小触发),否则某天日志量大,中途就因体积切了,生成 app.log.1,破坏“每天一个主文件”的约定。
- 设
MaxSize: 0后,lumberjack只响应Rotate()和MaxAge清理逻辑 - 但要注意:
MaxBackups别设太小(如1),否则零点切完,旧文件立刻被删,跨天瞬间可能丢最后几条日志 - Windows 下
os.Rename对打开的文件失败,lumberjack内部已处理Close()→Rename()→OpenFile()流程,但你要确保LocalTime: true和权限(0644)显式配置
真正难的不是写代码,是保证零点那几十毫秒内,所有 goroutine 都不往即将关闭的文件句柄写——所以加锁位置、Rotate() 调用时机、以及容器时区对齐,三者缺一不可。

















