lumberjack.MaxAge无法按天切日志,因其仅依据文件mtime删除旧备份,与写入时间无关;真正按天归档需手动拦截Write()检查日期变更并调用Rotate(),注意Close()、锁保护及Filename动态设置。

为什么直接用 lumberjack.MaxAge 无法按天切日志
MaxAge 不是“每天新建一个文件”,它只按文件系统 mtime 删除旧备份,和日志写入时间无关。比如凌晨没日志,mtime 不更新,当天文件可能被误删;NTP 校时导致时钟跳变,又可能漏删或错删。Filename 是固定字符串,不支持 app.%s.log 这类模板,lumberjack 本身压根不解析日期。
怎么让 lumberjack 真正按天归档(手动封装)
必须自己拦截 Write(),检查当前日期是否变更,再触发 Rotate():
-
lumberjack.Logger.Close()必须显式调用,否则缓冲区日志丢失;defer 在 main goroutine 快速退出时大概率来不及 - 每次 Rotate 前要
RWMutex.Lock(),防止并发写已关闭的文件句柄,报write: bad file descriptor -
Filename字段允许运行时修改,但得先Close()再重设,例如拼成/var/log/app/app-2026-11-02.log - 设置
MaxAge: 0关闭 lumberjack 自带清理,避免和你的日期逻辑冲突 - 日期检查别太频繁,用
sync.RWMutex缓存 today 字符串,每 10 秒查一次就够了
更稳的方案:别在 Go 进程里做轮转
把日志全写到 os.Stdout,轮转交给外部工具:
- systemd 服务配
StandardOutput=journal+logrotate,由 journald 自动按时间/大小归档 - Docker/K8s 环境下走 stdout +
fluentd或filebeat,它们原生支持按%Y-%m-%d切片、压缩、上传 - 运维统一控制路径、权限、保留策略,应用代码零耦合,避开所有 Close 遗漏、竞态、Windows
rename失败问题 - 结构化日志字段不受影响——JSON 行格式仍可被 collector 完整解析
Kratos + zap 场景下特别注意的坑
Kratos 的 log.Logger 接口只认 Log(level, keyvals interface{}) 方法,*zap.Logger 不实现它,硬传会 panic。必须封装 ZapLogger 结构体,内嵌 *zap.Logger 并映射 level(如 log.LevelInfo → zapcore.InfoLevel),把交替 keyvals("msg", "hello", "id", 123)转成 zap.Field 列表。末尾别漏 Sync(),尤其在 Log() 方法里同步调用。请求级字段如 trace_id 要在中间件中提取,用 zlogger.With() 注入 context,别在结构体指针上直接打日志,会丢结构化字段。











