应弃用gin.logger(),自定义中间件将日志字段传给zap或logrus:zap配lumberjack.writer需注意maxsize单位mb、maxbackups指文件数、localtime必须true;logrus配rotatelogs应避免软链冲突,生产环境建议用时间戳命名;记录耗时与状态码须在c.next()后,panic日志需确保日志中间件在recovery前注册。

gin.Logger() 不能直接切分日志,必须替换
gin 默认的 gin.Logger() 中间件只往 os.Stdout 写,不支持文件输出,更不支持按大小或时间切分。强行用 os.File 替换它的输出对象,会丢失请求上下文(比如 status、耗时、clientIP),也绕不开它内部硬编码的格式逻辑。
正确做法是:完全弃用 gin.Logger(),自己写一个等效中间件,把日志字段传给结构化日志库(如 zap 或 logrus),再由后者对接切割归档组件。
- zap 配合
lumberjack.Writer:适合高性能、低延迟场景,但需手动构造字段,不兼容 gin 原生格式 - logrus 配合
file-rotatelogs+lfshook:更贴近 gin 默认日志语义,lfshook能按 level 分发写入,但要注意rotatelogs的软链命名冲突问题
zap + lumberjack 切分归档的关键参数
lumberjack.Writer 是最常用的 zap 文件切分方案,但它不是自动轮转的“定时器”,而是基于单次写入触发判断——所以必须确保你的 zap logger 每次调用 Info()、Error() 等方法时,底层 writer 确实收到了字节流。
常见漏配项:
-
MaxSize单位是 MB,不是字节;设为100表示 100MB 后切新文件 -
MaxBackups控制保留几个历史文件,设为7不代表保留 7 天,而是最多 7 个归档文件(哪怕全是今天生成的) -
LocalTime必须设为true,否则归档文件名里的日期是 UTC,和本地运维习惯不符 - 不要依赖
MaxAge清理旧文件——它只在每次写入时检查,如果某天没日志,那天的文件永远不会被删
示例初始化片段:
writer := &lumberjack.Logger{
Filename: config.RootDir + "/" + config.Filename,
MaxSize: 100,
MaxBackups: 7,
MaxAge: 7,
LocalTime: true,
}
logrus + file-rotatelogs 的软链陷阱
用 file-rotatelogs 时,常配 WithLinkName 创建软链指向最新日志,比如 app.log → app.log.20260819。但这个软链是 rotatelogs 自己维护的,**gin 中间件里不能直接打开或重命名它**。
容易踩的坑:
- 多个进程(比如热重启、多实例)同时写同一个软链目标,会导致文件句柄错乱,出现日志丢失或重复写入
-
WithRotationTime(24 * time.Hour)不保证严格在 00:00 切分——它基于首次写入时间推算下一次,若服务凌晨启动,第一次切分可能在次日凌晨 -
lfshook默认不处理logrus.WarnLevel以下级别,如果中间件里用了logger.Debug(),这部分日志不会进文件
建议:生产环境避免软链,改用带时间戳的固定文件名(如 app.%Y%m%d.log),靠 MaxAge 自动清理。
中间件里记录耗时与状态码的时机很关键
所有自定义日志中间件都必须在 c.Next() 之后读取 c.Writer.Status() 和耗时,否则拿到的是 0 或未设置值。但有一个例外:c.Errors 在 panic 后可能已被 Recovery 中间件消费,如果你同时用了 gin.Recovery() 和自定义日志中间件,顺序错了就记不到 panic 日志。
安全顺序是:
- 先注册自定义日志中间件(如
GinLogger(zapLogger)) - 再注册
gin.Recovery(),确保 panic 发生后还能被日志中间件捕获到c.Errors - 或者干脆不用
gin.Recovery(),改用 zap 自带的zapr.New(zapLogger)封装 Recovery,保持日志风格统一
真正容易被忽略的是:当请求被某个前置中间件 abort(比如鉴权失败),c.Next() 不会执行,后续的耗时、status 字段就拿不到。这时应在 c.Abort() 前主动记录,或统一用 c.IsAborted() 判断补全。











