kratos中直接使用lumberjack.Logger会丢日志,因其未实现Sync()方法,需用zapcore.AddSync包装;必须设LocalTime:true、预建目录、显式优雅关闭以避免丢失。

kratos里直接用lumberjack.Logger会丢日志
kratos的log.Logger接口不直接接受lumberjack.Logger,也不能直接塞给zapcore.NewCore。它只认zapcore.WriteSyncer——而lumberjack.Logger实现了io.Writer,但没实现Sync()方法。zap在批量刷日志时会调Sync(),如果没这个方法,部分日志可能卡在缓冲区没写出去就结束了。
必须用zapcore.AddSync包装:
func getLogWriter() zapcore.WriteSyncer {
lj := &lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 10,
MaxBackups: 7,
MaxAge: 28,
LocalTime: true, // 关键:否则文件名和MaxAge都按UTC算
Compress: true,
}
return zapcore.AddSync(lj) // 这一步不能省
}
-
LocalTime: true必须显式设,否则归档文件名带的是UTC时间,排查时容易看错日期 - 目录
logs/得提前用os.MkdirAll("logs", 0755)创建,lumberjack不自动建目录 -
MaxSize单位是 MB,不是字节;设为0表示禁用大小触发,只靠MaxAge和手动Rotate()
按天切分日志文件名带日期?lumberjack默认不支持
lumberjack本身没有“每天零点新建app.log.2026-10-19”这种能力。它只会在触发轮转(比如大小超限或Rotate()被调)时,把当前文件重命名成app.log.2026-10-19_15-22-03这种带毫秒的时间戳格式——而且前提是LocalTime: true已开启。
想严格按天切、且文件名含YYYY-MM-DD,得自己加一层控制:
- 启动时用
time.Now().Format("2006-01-02")生成初始文件名,比如app.log.2026-10-19(注意:这需要你放弃Filename固定值,改用动态路径) - 起一个
time.Ticker每分钟检查是否跨天,一旦t.Day() != lastDay就调lj.Rotate() -
Rotate()是非阻塞的,但并发调用会panic,务必加sync.Mutex或用单个goroutine串行处理 - 第一次初始化后,当前日志仍叫
app.log(除非你手动重命名),归档文件才带时间戳
kratos服务退出时日志没刷完就终止了
常见现象是程序收到SIGINT或SIGTERM后立刻退出,lumberjack内部缓冲里的日志来不及fsync就丢了。这不是kratos的问题,而是没做优雅关闭。
必须显式关掉lumberjack,且不能依赖defer lj.Close()——main函数退出太快,goroutine可能根本没执行到。
- 用
signal.Notify监听os.Interrupt和os.Kill - 收到信号后,先调
logger.Sync()(如果是zap)或lj.Close()(如果是标准log) - kratos里推荐在
app.Run()之后补一手defer getLogWriter().Close(),但要确保这个WriteSyncer是可关闭的(zapcore.AddSync包装后的对象支持Close()) - 如果同时写控制台和文件,别用
io.MultiWriter直接拼,它不保证各writer的Close()顺序;分开管理更稳妥
多个微服务共用同一日志目录,文件锁冲突怎么办
lumberjack内部用os.OpenFile带os.O_APPEND | os.O_CREATE | os.O_WRONLY打开文件,Linux下多个进程写同一个文件不会报错,但轮转时Rotate()会尝试rename,这时可能因文件正被其他进程写入而失败,表现为invalid argument或text file busy错误。
生产环境多实例部署时,必须隔离日志路径:
- 每个服务实例用唯一标识区分日志名,比如
app-{hostname}-{pid}.log或app-{service-name}-{env}.log - 避免所有实例往
logs/app.log硬写,哪怕只是读取配置也容易踩坑 - 如果真要共享日志流(比如统一收集),应走syslog、gRPC或日志代理(如filebeat),而不是多个进程直写同一文件
- lumberjack不处理多进程竞争,它的设计假设是单进程写入
最常被忽略的一点:lumberjack的MaxAge清理逻辑是异步的,在Write()过程中可能触发mill()去删旧文件,这个动作本身不加锁——如果你在程序退出前没等它完成,旧文件可能残留,磁盘水位判断就失准了。


















