gin默认日志不支持自动轮转,因其底层gin.defaultwriter仅对接io.writer,不处理文件切分、保留策略或压缩,需借助lumberjack等专业库实现线程安全的write/close及大小/时间触发轮转。

为什么 Gin 默认日志不支持自动轮转
Gin 的 gin.DefaultWriter 底层是 os.Stdout 或任意 io.Writer,它本身不处理文件切分、保留天数、压缩等逻辑。直接写入文件(比如用 os.OpenFile)后,日志会无限追加,直到磁盘爆掉——这不是 Gin 的缺陷,而是设计使然:日志输出解耦,交由更专业的库处理。
所以你不能靠改 gin.SetMode() 或调用 gin.DebugPrintRouteFunc 解决轮转问题;必须替换底层 io.Writer,且新 Writer 要满足:线程安全、支持 Write()、能响应时间/大小阈值触发切割。
lumberjack.Logger 必须包装成 io.WriteCloser 才能塞给 Gin
lumberjack.Logger 实现了 io.Writer,但 Gin 的 gin.SetOutput() 接收的是 io.Writer,看起来能直接用。但实际运行时可能 panic,因为 Gin 在某些内部清理路径中会尝试调用 Close()(比如测试环境或热重载场景),而 lumberjack.Logger 没实现 io.Closer。
解决方法是套一层适配器:
type lumberjackWriteCloser struct {
*lumberjack.Logger
}
func (l *lumberjackWriteCloser) Close() error {
return l.Logger.Close()
}
然后这样设置:
lj := &lumberjackWriteCloser{
Logger: &lumberjack.Logger{
Filename: "logs/access.log",
MaxSize: 10, // MB
MaxBackups: 7,
MaxAge: 28, // days
Compress: true,
},
}
gin.SetOutput(lj)
-
MaxSize单位是 MB,不是字节;设为0表示禁用大小轮转(只靠时间) -
MaxAge和MaxBackups是独立触发条件:即使没到 28 天,备份数超 7 个也会删最老的 - 不要在
Filename里写./logs/...,lumberjack 不会自动创建目录,得提前os.MkdirAll("logs", 0755)
如何同时输出到控制台和轮转文件
直接用 io.MultiWriter 最简单,但要注意:Gin 日志默认带 ANSI 颜色码,写进文件会变成乱码(比如 \x1b[97m)。你需要区分环境:
- 开发环境:用
gin.DefaultWriter(带颜色) + 文件 writer → 颜色码污染文件 - 生产环境:关闭颜色,再用
io.MultiWriter
推荐做法是统一关颜色,再分流:
gin.DisableConsoleColor() // 关闭所有 ANSI 码
fileWriter := &lumberjackWriteCloser{...}
multiWriter := io.MultiWriter(fileWriter, os.Stdout)
gin.SetOutput(multiWriter)
如果你坚持开发期保留控制台颜色、又想文件干净,就得自己封装一个条件写入的 io.Writer,判断是否是终端输出再决定是否过滤 ANSI,这已超出 lumberjack 职责范围。
轮转失败的典型错误和排查点
常见现象是日志一直写在同一个文件里不动,或者报错 open logs/access.log: permission denied。
- 权限问题:确认进程对
logs/目录有wr权限,且Filename路径不含非法字符或空格 - Windows 下路径分隔符:用
filepath.Join("logs", "access.log")替代硬写"logs\access.log" - 忘记初始化 lumberjack:
lumberjack.Logger字段全零值时,MaxSize=0会导致永不轮转,必须显式赋值 - 并发写冲突:如果多个 Gin 实例(比如 fork 出的子进程)共用同一
Filename,lumberjack 无法协调,会丢日志甚至 panic
最易被忽略的是:lumberjack 的轮转是“写入时检查”,不是后台定时任务。也就是说,没有新请求写日志,就不会触发切割——哪怕过了 28 天,只要没新日志,旧文件就一直挂着。











