lumberjack是gin+zap轮转的唯一合理选择,因其是线程安全的io.writecloser,内置按大小/时间切割、压缩归档、备份保留功能,无缝对接zap的writesyncer和gin的writer接口,避免os.openfile+定时器导致的并发写冲突与日志丢失。

直接用 lumberjack.Logger 做轮转,别自己拼 os.OpenFile + 定时器 —— 它不处理并发写、不压缩旧文件、不清理过期备份,生产环境一压就丢日志。
为什么 lumberjack 是 Gin + Zap 轮转的唯一合理选择
lumberjack 不是日志库,而是一个线程安全的、带自动切割能力的 io.WriteCloser 实现。Zap 的 WriteSyncer 接口刚好能接它,Gin 的 LoggerWithConfig 和 RecoveryWithWriter 也只认 io.Writer。三方无缝咬合,没中间层损耗。
常见错误现象:有人用 os.OpenFile 配合 time.Ticker 每天重开一次句柄,结果高并发下多个 goroutine 同时写入旧文件、新文件打开失败、panic 堆栈写进访问日志里。
-
lumberjack.Logger内部用sync.RWMutex控制写入,所有操作原子 - 轮转触发时机由写入字节数(
MaxSize)或时间(MaxAge)驱动,非定时轮询 -
Compress: true会自动对归档文件做 gzip,节省 70%+ 磁盘空间 - 必须显式设置
LocalTime: true,否则归档文件名里的日期是 UTC,和运维排班对不上
lumberjack 配置参数怎么填才不踩坑
这几个字段不是可选的,漏一个就可能让日志卡住或爆盘:
-
Filename:必须是绝对路径,相对路径在 daemon 化后常指向/,权限失败且静默 -
MaxSize:建议设为100(MB),太小导致频繁切割、inode 耗尽;太大则单个文件难排查 -
MaxBackups:设为30,保留一个月,别设0—— 那等于不清理 -
MaxAge:设为7(天),和MaxBackups是“或”关系,谁先满足谁触发归档 -
LocalTime:务必设为true,否则2026-08-12_02-03-04.log这种名字根本没法按天查
示例片段:
lj := &lumberjack.Logger{
Filename: "/var/log/myapp/access.log",
MaxSize: 100,
MaxBackups: 30,
MaxAge: 7,
Compress: true,
LocalTime: true,
}
Zap 的 WriteSyncer 怎么桥接 lumberjack
Zap 不直接接受 *lumberjack.Logger,必须包装成 zapcore.WriteSyncer。别手写接口实现 —— 用 zapcore.AddSync 就够了,它内部已处理 Sync() 调用转发。
- 错误写法:
zapcore.AddSync(lj)直接传指针,但lumberjack.Logger没实现Sync()方法 → panic - 正确写法:
zapcore.AddSync(zapcore.Lock(lj)),Lock会补全Sync()并加锁保障刷盘安全 - 如果同时要输出到终端,用
io.MultiWriter组合:zapcore.AddSync(zapcore.Lock(io.MultiWriter(lj, os.Stdout))) - 禁用 ANSI 颜色:必须在
EncoderConfig里设EncodeLevel: zapcore.LowercaseLevelEncoder,否则终端颜色码会污染 JSON 日志
轮转后旧日志没被压缩/没删除?检查这三点
轮转逻辑本身很稳,但实际失效往往因为外部约束:
- 磁盘满或 inodes 耗尽:
df -i查 inode 使用率,lumberjack归档失败时不报错,只默默继续写原文件 - 文件权限不对:运行用户对
Filename所在目录无写权限,lumberjack创建新文件失败,但继续往旧文件追加 —— 看似“没轮转”,实则是权限问题 -
MaxAge和MaxBackups冲突:比如设了MaxBackups: 5但MaxAge: 30,旧文件可能因未达 30 天而一直不删,撑爆磁盘
最易忽略的是:轮转只作用于你传给 WriteSyncer 的那个 lumberjack.Logger 实例。如果你为 access log 和 error log 各建了一个 lumberjack.Logger,就得各自配置轮转参数,不能共用同一份配置。











