因为采样仅对zap.Logger生效,SugaredLogger完全绕过采样逻辑;中间件若混用Sugar()或Infow(),日志不进入Sampler,导致配置失效。

为什么用 zap.Logger 做中间件日志却没采样
因为采样只对 zap.Logger 生效,zap.SugaredLogger 完全绕过采样逻辑。很多中间件写法里混用了 Sugar() 或直接调 logger.Info()(其实是 Sugar 的别名),结果配置了采样也白搭。
常见错误:在 HTTP 中间件里用 log := logger.Sugar(),再调 log.Infow("request", "path", r.URL.Path) —— 这条日志压根不进 sampler。
- 确认所有中间件统一用
zap.Logger实例,禁用SugaredLogger - 高频路径(如每请求都打的日志)必须走
logger.Info("msg", zap.String("path", r.URL.Path))这种强类型写法 - 验证方式:在中间件里循环打 1000 条同模板日志(message 和字段 key 完全一致),观察输出是否被截断
如何给中间件日志配采样阈值
不能靠 zap.NewProduction() 或环境变量开关——它的采样策略是硬编码的(同模板每秒最多 100 条),且不可关闭。必须手动构造 Core。
关键点:采样发生在编码后、写入前,和日志级别无关;zap.WithLevel(zap.DebugLevel) 对采样毫无影响。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 关采样:用
zapcore.NewNopSampler()替换默认 sampler - 调阈值:传
zapcore.WithSampledLevel(zapcore.InfoLevel, 500),表示 Info 级别下同模板每秒最多记 500 条 - 注意:采样只匹配 message + 字段 key,
zap.String("req_id", id)中的id是动态值,不影响模板识别
中间件里怎么安全传动态字段做结构化日志
别用 zap.Any(),它触发反射、分配内存,破坏 zap 零分配设计,还让日志分析工具无法识别字段类型。
比如记录查询参数 r.URL.Query().Get("id") 返回空字符串,你误判为 nil 并转成 *string 塞进 zap.Stringer(),就会 panic:panic: runtime error: invalid memory address or nil pointer dereference。
- 字符串值不确定是否为空?统一用
zap.String("id", id)(对 nil 安全,输出空字符串) - 要格式化复杂 struct?先判空,再用
zap.Object("req", req),前提是req实现了MarshalLogObject - 时间字段必须用
zap.Time("ts", time.Now()),别用zap.String("ts", t.Format(...))—— 后者每次调用都 new 字符串
文件输出时中间件日志为啥丢最后几条
直接把 os.File 塞进 WriteSyncer 是典型陷阱:进程崩溃时内核缓冲区未刷盘,最后几条日志就没了;而且 os.File 不支持轮转,单文件无限增长。
更隐蔽的问题是:如果中间件用了 lumberjack.Logger 但没套 zapcore.AddSync(),logger.Sync() 调用会静默失败,根本不会刷盘。
- 必须用
zapcore.AddSync(&lumberjack.Logger{MaxSize: 100, MaxAge: 7})封装 lumberjack 实例 - 确保所有派生子 logger(比如
logger.With(zap.String("service", "api")))在进程退出前都调一次Sync() - 开发阶段别用
zap.NewProduction()看日志——它面向采集器,不是人眼;本地无条件用zap.NewDevelopment()
zap.Logger、只走强类型字段、只封装带 AddSync 的 WriteSyncer。

















