因为gin.Default()使用标准io.Writer(如os.Stdout),其本身无轮转逻辑;Logrus也不自带切割能力,需依赖file-rotatelogs等外部组件提供支持轮转的io.Writer实例,或手动实现io.Writer接口注入切分逻辑。

为什么 gin.Default() 的日志不自动分割?
因为 gin.Default() 用的是标准 io.Writer 输出(比如 os.Stdout),它本身没有文件轮转逻辑。Logrus 也不自带切割能力,必须靠外部组件或手动实现 io.Writer 接口来触发按时间/大小切分。
logrus + file-rotatelogs 实现按天分割
这是最轻量、稳定且被广泛验证的组合。关键不是“集成 logrus”,而是让 logrus 把日志写进一个支持轮转的 io.Writer。
-
file-rotatelogs提供的是io.Writer实例,不是 logger;直接传给logrus.SetOutput()即可 - 注意
rotatelogs.New()返回的 writer 默认不带软链(WithLinkName必须显式调用,否则找不到最新日志) - 避免用
lumberjack和file-rotatelogs混用——前者按大小切,后者按时间切,混用会导致行为不可控 - 示例片段:
logWriter, _ := rotatelogs.New(
"logs/gin-%Y-%m-%d.log",
rotatelogs.WithLinkName("logs/gin-current.log"),
rotatelogs.WithMaxAge(7*24*time.Hour),
rotatelogs.WithRotationTime(24*time.Hour),
)
logger.SetOutput(logWriter)
gin.LoggerWithConfig 与 logrus 冲突吗?
会。如果你同时注册了 gin.LoggerWithConfig 中间件和自定义的 logrus 中间件,请求会被记录两次:一次是 gin 原生格式,一次是你用 logrus 打的结构化日志。
- 生产环境应只保留一种:推荐停用
gin.Logger(),改用纯 logrus 中间件 - 若要用
gin.LoggerWithConfig,就别再挂 logrus 日志中间件,否则c.Next()前后各打一遍 -
SkipPaths和Skip函数在 gin 原生日志里有效,但在 logrus 中间件里得自己实现(比如在中间件开头加if c.Request.URL.Path == "/healthz" { c.Next(); return })
logrus 日志文件权限和路径问题
Go 进程启动时工作目录不一定是项目根目录,"logs/app.log" 可能写到意想不到的位置,甚至因权限不足静默失败。
- 务必用
filepath.Abs()或os.Executable()解析出绝对路径 - 创建日志目录前检查并
os.MkdirAll(dir, 0755),否则rotatelogs初始化会 panic - Linux 下如果以 systemd 启动,需确认
User=和日志目录属主一致,否则open logs/xxx.log: permission denied - 不要依赖
logrus.SetLevel()控制 gin 请求日志开关——它只影响 logrus 自己打的日志,不影响gin.Logger()行为
WithFields() 里传入的字段名不能含空格或特殊符号,否则 JSON 格式会损坏;而 rotatelogs 的时间模板里 %Y 是大写,写成 %y 就不会按年份切分。


















