Zap 与 Echo 整合的关键在于字段可索引、错误上下文不丢失、日志按 level 分离且不重复;需重写 HTTPErrorHandler 显式调用 zap.Error() 补全字段,统一 status 命名,并用双 core 实现 info/error 分文件写入。

zap 和 Echo 整合的关键不是“能不能用”,而是**字段是否可索引、错误上下文是否不丢失、日志是否按 level 分离且不重复写入**。直接套用 zap.NewProduction() 或在中间件里无脑 logger.With(),上线后查不到 error、磁盘爆满、Loki 里搜 status:500 返回空——这些都不是配置没生效,是结构错了。
为什么 Echo 的 HTTPErrorHandler 必须重写才能用好 Zap
默认的 Echo.HTTPErrorHandler 在 panic 恢复后只调用 echo.Logger.Error(),它和 zap.Logger 完全无关;你传进去的 *zap.Logger 如果没被显式接入错误处理链,panic 日志就只会走 Echo 自带的文本 logger,字段全丢、caller 不带、level 固定为 ERROR(无法区分 warn/error/fatal)。
正确做法是替换 HTTPErrorHandler,并在其中调用 zap.Error() 显式注入字段:
- 必须用
logger.Error("http_error", zap.Error(err), zap.Int("status", code)),而不是logger.Error(err.Error()) - status 字段名要统一(比如固定用
http_status,别混用status/code),否则 Loki 或 ES 无法聚合 - 不要在 handler 内部再手动
logger.Error()—— 否则同一错误会记两次,且第二次没http_status字段 - 如果用了 request ID,确保它已在 context 中注入(如通过
c.Set("req_id", uuid)),并在 error handler 中用zap.String("req_id", c.GetString("req_id"))补上
如何让 Info 和 Error 日志分写到不同文件(error.log / info.log)
Zap 本身不支持“按 level 自动路由”,必须手动构造两个 zapcore.Core,分别绑定不同 io.Writer 和 LevelEnablerFunc。常见错误是只建一个 writer 然后靠 if 切换,结果 info 日志漏掉 error 级别上下文(比如 debug 时找不到 error 前的请求 trace)。
实操要点:
立即学习“go语言免费学习笔记(深入)”;
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 定义两个
lumberjack.Logger:一个infoWriter(Filename: "/var/log/myapp/info.log"),一个errorWriter(Filename: "/var/log/myapp/error.log") - 为
errorCore设置LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel }) - 为
infoCore设置LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl - 合并 cores 时用
zapcore.NewTee(infoCore, errorCore),不是zapcore.NewCore(...)单 core - 务必确认两个 writer 的
MaxSize、MaxAge、LocalTime: true保持一致,否则归档行为错乱
字段名下划线 vs 小驼峰:Kibana 查不到日志的真正原因
zap.NewProduction() 默认字段名是 httpStatus、reqId、caller,但 Kibana、Loki、阿里 SLS 默认按 http_status、req_id 建索引。字段名不匹配 → 字段未被提取 → 全文搜索都救不了。
别用 zap.String("http_status", ...) 手动转——字段名必须在编码器层统一,否则 Info 和 Error 日志字段格式不一致,聚合失效:
- 改用自定义
EncoderConfig:zapcore.NewJSONEncoder(zapcore.EncoderConfig{LevelKey: "level", TimeKey: "ts", MessageKey: "msg", CallerKey: "caller", FunctionKey: "func", LineKey: "line", EncodeLevel: zapcore.LowercaseLevelEncoder}) - 关键字段键名(如
http_status、req_id)必须在EncoderConfig中设为对应 key,不能靠zap.String()临时覆盖 - 如果你用的是
lumberjack+JSONEncoder,字段名错配会导致日志解析失败,fluent-bit 直接丢弃整条日志
中间件里 logger.With() 调用太勤,性能反被拖垮
每个 logger.With(zap.String("path", c.Path())) 都会创建新 *zap.Logger 实例,内部做字段合并、指针拷贝。QPS 上千时,这部分分配开销可能占日志路径总耗时 30%+,GC 压力明显上升。
更稳的做法是预置通用字段 + 按需叠加:
- 启动时用
baseLogger := logger.With(zap.String("service", "myapp"), zap.String("env", "prod"))构建基础 logger - 中间件中只叠加请求级字段:
reqLogger := baseLogger.With(zap.String("path", c.Path()), zap.String("method", c.Request().Method)) - 绝对不要在 for 循环或高频 callback 里反复调用
With();字段多时优先用logger.Info("msg", fields...)直接传参 - 如果用了 Gin/Echo 官方中间件(如
echolog.ZapLogger()),确认它内部是否已做字段预置 —— 很多封装只是简单With(),没优化

















