1. zaptest/observer:测试断言日志(官方示范:自定义 Core)
observer.go 包注释一语道破:"一个把日志条目保存在内存里的、与编码格式无关的 Core"------不关心输出长什么样,只关心"记了什么"。
1.1 结构
Go
// ObservedLogs:并发安全的有序日志集合(observer.go:30-33)
type ObservedLogs struct {
mu sync.RWMutex
logs []LoggedEntry
}
// logged_entry.go:LoggedEntry = Entry + Context []Field(字段被"解码"成结构化形式)
type LoggedEntry struct {
zapcore.Entry
Context []zapcore.Field
}
// New(observer.go):
func New(lvl zapcore.LevelEnabler) (zapcore.Core, *ObservedLogs)
// 内部用 memoryEncoder(zapcore/memory_encoder.go)把 Field 编码成 map/切片再还原
1.2 查询 API(都是"拷贝后返回",断言安全)
|------------------------------------------------------------------|---------------------|
| 方法 | 作用 |
| Len() / All() / TakeAll() | 数量 / 全量拷贝 / 取走并清空 |
| AllUntimed() | 时间清零------断言不被时间戳干扰 |
| FilterLevelExact/FilterMessage/FilterMessageSuffix/FilterField | 链式过滤 |
1.3 用法与源码启示
Go
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
Login(logger, "alice")
for _, e := range logs.FilterMessage("login ok").All() {
// e.Context[0] 是 trace_id 字段...
}
源码启示 :observer 的 Core 在 Write 里把 Field 用 memoryEncoder 落成 map[string]interface{}------这就是"编码无关"的实现方式。你的业务测试、或"日志审计"类需求都可以照抄这个模式。
2. zaptest:让日志进 t.Log
zaptest/logger.go:
Go
// NewLogger(t) 构造一个把日志写到 testing.T 的 logger
// 核心:testingT 适配器 + writerSyncer{t.Log}
价值:单测跑挂时,失败现场的日志自然出现在 go test -v 输出里;测试通过时不刷屏。还有 timeout.go(带超时的 t 封装)等周边。
3. zapio:logger 变身 io.Writer
zapio/writer.go:51-63:
Go
type Writer struct {
Log *zap.Logger // 目标 logger(必填)
Level zapcore.Level // 写入级别,默认 Info
buff bytes.Buffer // 行缓冲
}
行为(75-100):按换行切分 ,每凑满一行发一条日志;没遇到 \n 就先攒着(Sync/Close 强制冲刷)。
Go
// 典型用途(源码注释 38-49):接管子进程输出
writer := &zapio.Writer{Log: logger, Level: zap.DebugLevel}
defer writer.Close()
cmd := exec.CommandContext(ctx, "ping", "-c", "4", "example.com")
cmd.Stdout = writer // 每行输出 = 一条结构化日志
cmd.Stderr = writer
cmd.Run()
细节:Write 先查 Enabled(77-79 行)------级别禁用时直接吞掉,连切分都不做(又是那个快路径思想)。
4. zapgrpc:适配 gRPC 日志接口
zapgrpc/zapgrpc.go:
Go
// 级别映射表(31-36)
var _grpcToZapLevel = map[int]zapcore.Level{
grpcLvlInfo: zapcore.InfoLevel,
grpcLvlWarn: zapcore.WarnLevel,
grpcLvlError: zapcore.ErrorLevel,
grpcLvlFatal: zapcore.FatalLevel,
}
type Logger struct { // 适配器:把 grpclog 的方法翻译给 zap
delegate *zap.Logger
// 内部按级别分组 printer(print/printf/fatal...),支持 Option 微调
}
用法(07 篇提过):grpclog.SetLoggerV2(zapgrpc.NewLogger(logger))。这个包是"接口适配器"类扩展的标准样例:不碰 Core/Encoder,纯 API 翻译。
5. exp:实验区
exp/ 放不稳定的新能力(有独立 CHANGELOG,不承诺兼容):
Go
sl := slog.New(zapslog.NewHandler(zapLogger, nil))
sl.Info("via slog", "k", "v") // 走 zap 的编码、采样、输出
- zapslog :
slog.Handler实现------把任意 slog 调用转发给 zap Core。
意义:新代码写标准库 API,底层保留 zap 的性能与生态------渐进迁移的标准姿势
- zapfield :字段构造函数的实验性增强(如
zap.Field组合工具)
6. 实战:自己动手写两个扩展
6.1 敏感信息打码 Core(装饰器模式)
Go
// 需求:输出前把 password/token 字段的值替换为 ***
type redactCore struct {
zapcore.Core
sensitive map[string]struct{}
}
func (c redactCore) With(fields []zapcore.Field) zapcore.Core {
return redactCore{c.Core.With(fields), c.sensitive}
}
func (c redactCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
filtered := make([]zapcore.Field, 0, len(fields))
for _, f := range fields {
if _, ok := c.sensitive[f.Key]; ok {
f = zap.String(f.Key, "***")
}
filtered = append(filtered, f)
}
return c.Core.Write(ent, filtered)
}
// 组装
core := redactCore{
Core: baseCore,
sensitive: map[string]struct{}{"password": {}, "token": {}},
}
logger := zap.New(core)
logger.Info("login", zap.String("password", "123456"))
// {"msg":"login","password":"***"}
注意局限:只处理日志点字段,With 进上下文的字段已被预编码成字节(13 篇)------要对上下文生效就得在 Encoder 层动刀,或约定敏感字段只走日志点。
6.2 自定义 Encoder:logfmt 风格(实现接口)
Go
// 需求:key=value 格式(类似 syslog/logfmt)
type logfmtEncoder struct {
zapcore.EncoderConfig
*buffer.Pool // 简化:直接内嵌池
}
func (e logfmtEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
buf := bufferpool.Get()
buf.AppendString("time="); buf.AppendTime(ent.Time, time.RFC3339)
buf.AppendString(" level="); buf.AppendString(ent.Level.String())
buf.AppendString(" msg="); buf.AppendString(ent.Message)
for _, f := range fields {
buf.AppendByte(' ')
buf.AppendString(f.Key); buf.AppendByte('=')
// 简化:只演示 string/int;完整实现按 f.Type switch
switch f.Type {
case zapcore.StringType: buf.AppendString(f.String)
case zapcore.Int64Type: buf.AppendInt(f.Integer)
}
}
buf.AppendString("\n")
return buf, nil
}
// 还需实现 ObjectEncoder 的几十个方法(可内嵌 *zapcore.jsonEncoder 借力,或像 consoleEncoder 那样组合)
// 注册进 Config 体系
zap.RegisterEncoder("logfmt", func(cfg zapcore.EncoderConfig) (zapcore.Encoder, error) {
return logfmtEncoder{cfg, nil}, nil
})
// 之后 Config.Encoding = "logfmt" 即用
工程提示:完整实现 ObjectEncoder 工作量不小------组合(内嵌 jsonEncoder)优于重写,consoleEncoder 就是这么干的。
6.3 扩展点选择指南
|------------------|----------------------------|--------------------------|
| 需求 | 扩展点 | 参考 |
| 改输出格式 | Encoder | consoleEncoder |
| 改行为(过滤/脱敏/采样/审计) | Core 装饰器 | sampler / observer |
| 新输出目标 | WriteSyncer + RegisterSink | 06 篇 kafka/tcp 示例 |
| 字段级编码 | ObjectMarshaler | 04 篇 |
| 翻译外部日志 API | 适配器 | zapgrpc / RedirectStdLog |
| 指标/告警 | Hooks 或 Core | options.go / hooked |