在 Kratos 中直接使用 zap.NewProduction() 或 zap.NewDevelopment() 创建的 zap.Logger 会 panic,必须封装实现 log.Logger 接口:定义 ZapLogger 结构体内嵌 zap.Logger,映射 level、转换 keyvals 为 zap.Field,末尾调 Sync();文件轮转需用 lumberjack.Logger + zapcore.AddSync,路径须绝对,MaxSize 设 100MB,MaxAge=7、MaxBackups=30、LocalTime=true;生产环境禁用 zap.NewDevelopment(),时间编码用 zapcore.ISO8601TimeEncoder;请求级字段(如 trace_id)应在中间件中提取并用 zlogger.With() 注入 context,避免结构体指针和格式化丢失结构化字段。

直接在 Kratos 里用 zap.NewProduction() 或 zap.NewDevelopment() 创建 logger 后塞给 log.NewHelper() 是错的——Kratos 的 log.Logger 接口只认 Log(level, keyvals ...interface{}) 方法,Zap 的 *zap.Logger 不实现它,硬传会 panic。
必须实现 Kratos 的 log.Logger 接口
Kratos 要求日志对象满足 log.Logger 接口,而 Zap 原生 logger 是结构体指针,不带该方法。你得自己封装一层:
- 定义一个结构体(比如
ZapLogger),内嵌*zap.Logger并实现Log(level log.Level, keyvals ...interface{}) -
level需映射为zapcore.Level:例如log.LevelInfo → zapcore.InfoLevel,log.LevelError → zapcore.ErrorLevel -
keyvals是交替的 key/value 对(如"msg", "hello", "id", 123),需用zap.Any()或类型判断转成zap.Field列表,再调logger.Log() - 别漏掉
Sync()调用时机:建议在Log()方法末尾或单独暴露Sync()方法供上层调用
文件轮转必须用 lumberjack.Logger + zapcore.AddSync
直接写文件或用 os.Stdout 无法自动切日志、删旧、压缩——Zap 本身不处理轮转,全靠外部 io.Writer 实现:
-
Filename必须是绝对路径(如/var/log/myapp/app.log),相对路径在 daemon 模式下常写到根目录或失败静默 -
MaxSize: 100(单位 MB)比1024更稳妥:太大导致单文件臃肿难排查,太小引发频繁 IO 切割抖动 -
MaxAge: 7和MaxBackups: 30是较平衡组合:保留最近 7 天内生成的所有备份,但最多存 30 个文件(防突发日志暴涨) - 务必设
LocalTime: true,否则文件名按 UTC 时间命名,在中国区服务器上归档时间错乱
开发与生产环境不能共用同一套初始化逻辑
zap.NewDevelopment() 和 zap.NewProduction() 底层编码器完全不同,混用会导致性能断崖下跌:
- 生产环境必须用
zap.NewProduction()初始化 core:它禁用颜色、堆栈、缩进,启用缓冲和高效 JSON 编码;用zap.NewDevelopment()上线,CPU 使用率会翻 2–3 倍,ELK 解析失败率飙升 - 开发时若需要 caller 行号,加
zap.AddCaller()和zap.AddCallerSkip(2)即可,不用换整个 encoder - 时间格式必须用
zapcore.ISO8601TimeEncoder,别手写time.Now().Format("2006-01-02...")——每次调用都触发内存分配,压测中 GC 压力陡增 - 避免在 HTTP handler 里每次 new
*zap.Logger:encoder、sink、goroutine 全部重建,开销远超日志本身
Kratos 中注入 trace_id 等请求级字段要绕过 context.WithValue
Kratos 默认推荐用 log.With() 绑定全局字段,但请求级字段(如 trace_id)不能这么干——它返回新 log.Logger,而你封装的 ZapLogger 没法自动继承内部 *zap.Logger 的字段合并能力:
- 正确做法是在中间件中提取
X-Trace-ID,为空则生成 UUID,然后调zlogger.With(zap.String("trace_id", tid))得到新*zap.Logger - 把这个新实例挂到 Kratos 的
http.Request.Context()里(不是context.WithValue,而是用 Kratos 的transport.FromContext(r.Context())提取后 set 进去) - 下游 handler 用
log.NewHelper()包装这个带字段的 logger,就能自动透传 - 字段名统一小写+下划线(如
trace_id),确保 Loki/ELK 能正确提取;别传结构体指针:zap.Any("user", &u)可能引发竞态
最易被忽略的一点:Kratos 的 log.Helper 在调 Infof() 或 Errorf() 时,会把格式化后的字符串当 msg 字段塞进去,而原生 Zap 的 Info() 不做这层包装。如果你封装的 ZapLogger.Log() 没处理好 fmt.Sprintf 类型的 keyvals,就会丢掉结构化能力——日志看着有内容,实际查不到字段。


















