echo中必须用context.withvalue()将traceid注入c.request().context()并调用c.setrequest()绑定,日志器、http客户端及goroutine均需显式传递该上下文,否则traceid无法透传至下游服务。

直接在 Echo 中间件里调 logger.Info() 不行——它打不出请求上下文字段,TraceID、耗时、状态码全丢。必须让每个请求都持有带上下文的 *zap.Logger 实例,且该实例要预置通用字段、支持按需叠加请求级字段。
中间件里别 new logger,要 With() 复用并注入请求字段
高频创建 *zap.Logger 实例(比如每次请求都 logger.With(zap.String("req_id", id)))会触发内存分配,压测中 GC 压力明显上升。正确做法是:
- 启动时初始化一个全局
*zap.Logger,预置service_name、env等静态字段 - 在中间件里只调一次
logger.With(),叠加req_id、remote_ip、user_agent等动态字段 - 把新实例挂到
c.Set("logger", reqLogger),后续 handler 用c.Get("logger").(*zap.Logger)拿,避免重复构造 - 切忌在中间件里对每个请求都调
logger.With().With().With()—— 字段合并有开销,应一次性叠加完
如何让 Zap 自动带上 TraceID 而不改每行日志代码
靠 c.Set("trace_id", ...) 不够,Zap 不会自动读这个值。真正生效的方式是让日志器感知当前请求的 context.Context:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 定义私有 key 类型:
type traceKey struct{},避免字符串 key 冲突 - 在 TraceID 中间件中:用
context.WithValue(c.Request().Context(), traceKey{}, traceID)创建新 ctx,再调c.SetRequest(c.Request().WithContext(newCtx))绑回请求 - 日志中间件里从 context 取值:
traceID, _ := c.Request().Context().Value(traceKey{}).(string),然后reqLogger = logger.With(zap.String("trace_id", traceID)) - 下游 HTTP 调用也必须透传这个 context,否则链路断在第一跳
写入文件时 lumberjack 配置错一个参数就归档失效
Zap 本身不轮转文件,全靠 lumberjack.Logger。常见错误是以为设了 MaxAge: 7 就能“每天一个文件”,其实它只删“最老的、超过 7 天的文件”,不生成新文件:
-
Filename必须是绝对路径,相对路径在 daemon 模式下可能写到/下导致权限失败 -
MaxSize单位是 MB,建议设为100(不是 1024),太大难排查,太小频繁 IO 切割 -
LocalTime: true必须显式开启,否则文件名用 UTC 时间,中国区服务器上归档时间错乱 -
MaxBackups: 30和MaxAge: 7配合更稳:最多存 30 个文件,但其中超过 7 天的会被优先清理 - 包装时必须用
zapcore.AddSync(&lumberjack.Logger{...}),裸传*lumberjack.Logger会 panic
日志采样默认开着,高频请求可能静默丢日志
zap.NewProduction() 默认启用 sampling,每秒超过 100 条相同日志(如某条 error 反复出现)就会被丢弃。线上 debug 时发现“明明出错了却没日志”,大概率是这个开关在起作用:
- 关闭采样:
zap.NewProductionConfig().DisableSampling = true - 或调大阈值:
cfg.Sampling = &zap.SamplingConfig{Initial: 100, Thereafter: 100} - 注意:采样只针对相同 msg+level 的日志,不影响 TraceID、req_id 等结构化字段的输出
- 如果用了
lumberjack+ 异步 wrapper(如zapcore.NewTee),进程退出前务必logger.Sync(),否则最后一段日志会截断
最易被忽略的一点:所有异步操作(goroutine、http.Client.Do、database.Query)都必须显式传递带 TraceID 的 context.Context,否则日志里看着有 TraceID,下游服务日志却是空的——链路在第一个 go 关键字就断了。










