因为Zap不提供自动分文件逻辑,zap.NewProduction()返回单core实例;需手动构造多个Core,分别绑定独立Writer和LevelEnablerFunc,避免error日志重复写入info.log。

为什么直接用 zap.NewProduction() 无法按 level 分文件
因为 Zap 本身不提供“自动分文件”逻辑,zap.NewProduction() 返回的是一个单 core 实例,所有日志都走同一个 writer 和 encoder。想让 error.log 只写 error 级别、info.log 写 info 及以上,必须手动构造多个 zapcore.Core,每个绑定独立的 io.Writer 和 LevelEnablerFunc。
常见错误是只配两个 writer(infoWriter 和 errorWriter),但没给 error core 设置拦截器,结果 error 日志既进了 error.log,又重复进了 info.log。
- 正确做法:为 error core 使用
zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel }) - info core 则用
zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.InfoLevel && lvl - 务必注意:Zap 的 level 是包含关系,
ErrorLevel会同时满足InfoLevel条件,所以拦截顺序和范围必须严格互斥
lumberjack.Logger 的 MaxAge 不是“按天归档”
MaxAge: 7 表示“文件最老存活 7 天”,不是“每天生成一个新文件”。如果某天完全没有日志写入,就不会创建当天的文件,MaxAge 也就无从触发清理——它只对已存在的文件按 mtime 判断是否过期。
这导致两种典型问题:磁盘空间突然暴涨(因旧文件未被删)、排查时发现“上周三的日志不见了”(其实那天根本没写日志,自然没生成文件)。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
-
Filename必须是绝对路径,否则在 systemd 或容器中常写到/下,权限失败或覆盖系统文件 -
LocalTime: true必须显式设为true,否则文件名用 UTC 时间,中国区服务器上看到的2026-08-31实际可能是北京时间 9 月 1 日凌晨 -
MaxSize: 100(MB)比1024更稳妥:太大拖慢切割,太小导致每分钟切一次,IO 毛刺明显
中间件里别对每个请求调用 logger.With()
logger.With() 每次都会合并字段、分配新结构体,在高并发场景下是可观的堆分配来源。尤其在 Echo 的 middleware 中,它可能被每秒数千次调用。
真正该做的,是把通用字段(如 service_name、env、hostname)在应用启动时就预置进 root logger;请求级字段(如 req_id、path、method)则通过 c.Set("logger", logger.With(...)) 注入上下文,后续 handler 直接取用,不再重复 With()。
- 错误示范:
logger := logger.With(zap.String("req_id", uuid.NewString()))放在 middleware 开头 - 正确做法:在 middleware 中只做一次
With(),然后c.Set("logger", logger),handler 中用c.Get("logger").(*zap.Logger)取出复用 - 更优解:用
echo.HTTPErrorHandler替换默认错误处理,内部直接调logger.Error("http_error", ...),避免 panic 恢复后丢失字段
字段命名不一致会让 Kibana / Loki 查不到日志
Zap 默认 key 名是小驼峰(httpStatus、reqId),但绝大多数日志平台(Loki、Datadog、阿里 SLS)索引规则默认匹配下划线风格(http_status、req_id)。字段名对不上 → 字段未被提取 → status:500 这种查询永远为空。
不要试图在每次 Info() 时手动转字段名(比如写 zap.String("http_status", ...)),维护成本高且易漏。统一改 encoder 配置才是正解。
- 替换默认 encoder:
zapcore.NewJSONEncoder(zapcore.EncoderConfig{LevelKey: "level", TimeKey: "ts", MessageKey: "msg", CallerKey: "caller", FunctionKey: "func", LineKey: "line"}) - 关键字段需显式映射:
LevelKey: "level"→"level": "error",而非默认的"level": "ERROR"(大写影响筛选) - 如果用了
rk-boot,直接在boot.yaml里设encoding: json+outputPaths,它底层已帮你做了字段标准化

















