直接用 zap.Logger 会 panic,因其未实现 log.Logger 接口的 Log 方法;需封装 ZapLogger 结构体并实现 Log、映射 level、配对 keyvals 为 zap.Field、调用 Sync;通过 log.With 注册 Valuer 自动注入 trace_id 等字段;用 log.NewFilter 统一脱敏敏感信息;keyvals 长度必须为偶数,否则 panic。

为什么直接用 zap.Logger 会 panic
因为 Kratos 要求日志对象实现 log.Logger 接口,而 zap.Logger 是结构体指针,不带 Log(level, keyvals interface{}) 方法。硬传 zap.NewProduction() 给 log.NewHelper() 会直接 panic。
必须自己封装一层:定义结构体(比如 ZapLogger),内嵌 *zap.Logger,并实现 Log 方法:
-
level映射为zapcore.Level(如log.LevelInfo → zapcore.InfoLevel) -
keyvals是平铺数组("msg", "ok", "uid", 123),需按两两配对转成zap.Field列表 - 末尾务必调
logger.Sync(),否则日志可能丢失
如何让 trace_id、service_name 等字段自动出现
不用在每条日志里手动写 "trace_id", tracing.GetTraceID(ctx)——用 log.With() 注册 Valuer 就行。
Valuer 是函数类型 func() interface{},注册后所有经 Helper 发出的日志都会自动带上该字段:
log.With(logger, "trace_id", func() interface{} { return middleware.GetTraceID(ctx) })log.With(logger, "service_name", "user-svc", "env", "prod")- 注意:全局字段注册一次即可,放在初始化 logger 的地方,不要每次请求都重复绑定
怎么安全脱敏手机号、token 这类敏感字段
靠业务代码判断 "phone" 或 "auth_token" 并替换,极易漏掉。应该用 log.NewFilter() 在日志真正输出前统一拦截。
Filter 是链式执行的,常见用法:
-
log.FilterKey("phone", "auth_token", "password")—— 匹配 key 名,值自动替换成"***" -
log.FilterValue("secret_.*")—— 正则匹配 value,整条字段删除 - 多个 Filter 可组合:
log.NewFilter(logger, f1, f2, f3),顺序影响结果(先过滤再脱敏更安全)
最容易被忽略的 panic 原因:keyvals 长度必须是偶数
Logger.Log() 按两两配对解析 keyvals:索引 0 是 key、1 是 value、2 是 key……所以 len(keyvals) % 2 == 0 是硬性要求。
动态拼接时很容易出错,比如:
logger.Log(log.LevelInfo, "msg", "ok", "uid") // ❌ 少一个 value,panic
建议:
- 所有业务日志走
Helper(它内部已做校验) - 自定义
Logger实现时,在Log()开头加if len(keyvals)%2 != 0 { panic(...) } - 避免用
append()拼接 keyvals 后直接传入,应显式检查长度











