zap源码阅读

zap是Uber开源的golang日志库,以结构化日志、高性能、可扩展的特点受到许多开发者的欢迎。

使用

创建logger

zap对外提供了两个不同的日志对象,强类型的Logger和简化使用方式的SugaredLogger

  • Logger创建

    • 低级API

      func New(core zapcore.Core, options ...Option) *Logger

    • 通过配置创建
      使用了builder模式,通过Config对象的Build方法创建

      func (cfg Config) Build(opts ...Option) (*Logger, error)

      配置对象可以使用预设的NewDevelopmentConfigNewProductionConfig创建,也可以反序列化json或yaml来创建
      Option提供了动态修改Logger配置的能力,定义为接口,每个实现在闭包中修改Logger对象

      1
      2
      3
      type Option interface {
      apply(*Logger)
      }
    • 构造带固定上下文的Logger

      func (log *Logger) With(fields ...Field) *Logger

  • SugaredLogger

    • 封装了Logger,可以和Logger互相转换

      func (log *Logger) Sugar() *SugaredLogger
      func (s *SugaredLogger) Desugar() *Logger

      内部通过sweetenFields方法把(string, interface{})类型的kv参数转换为强类型的Field

      func (s *SugaredLogger) sweetenFields(args []interface{}) []Field

    • 通过With方法构造一个带固定上下文的SugaredLogger

      func (s *SugaredLogger) With(args ...interface{}) *SugaredLogger

打印日志

  • Logger

    msg传入简单字符串,其他日志信息通过Field包装起来
    func (log *Logger) Info(msg string, fields ...Field)

    1
    logger.Info("msg", zap.String("foo", "bar"))
  • SugaredLogger

    格式化msg的方法
    func (s *SugaredLogger) Infof(template string, args ...interface{})

    1
    sugar.Infof("Failed to fetch URL: %s", url)

    带kv参数的方法
    func (s *SugaredLogger) Infow(msg string, keysAndValues ...interface{})

    1
    2
    3
    4
    5
    6
    sugar.Infow("Failed to fetch URL.",
    // Structured context as loosely typed key-value pairs.
    "url", url,
    "attempt", 3,
    "backoff", time.Second,
    )

代码

分层

zapcore 封装日志的Core接口和实现,以及level、entry、encoder、field等基础功能
zap 提供高层接口,包括sugarlogger、配置、输出接口Sink和实现、HTTP修改level接口等
其他包提供支持功能。buffer实现了一个默认size为1k的buffer池。zapgrpc封装了与grpclog兼容的日志。

创建流程

Logger结构体很轻量,核心的core是一个接口,创建一个新的对象开销不大,一般使用也可以重用Logger对象。

1
2
3
4
5
6
7
8
9
10
11
12
type Logger struct {
core zapcore.Core

development bool
name string
errorOutput zapcore.WriteSyncer

addCaller bool
addStack zapcore.LevelEnabler

callerSkip int
}
  • New方法
    func New(core zapcore.Core, options ...Option) *Logger

    根据传入的参数赋值,然后调用WithOptions设置Logger。

  • Config.Build
    func (cfg Config) Build(opts ...Option) (*Logger, error)

    根据配置构造encoder,输出对象,然后调用zapcore.NewCore创建core,最后根据参数opts来设置Logger

    encoder的创建使用了简单工厂,内部维护了一个字符串到构造函数的映射,根据配置来决定使用哪个encoder。可以用RegisterEncoder注册新的encoder。

    输出对象也使用了简单工厂,内部维护了URL的scheme到构造函数的映射。通过url.URL来决定输出对象接口Sink的实现。可以用RegisterSink来注册新的Sink实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func (cfg Config) Build(opts ...Option) (*Logger, error) {
    enc, err := cfg.buildEncoder()
    // error handle

    sink, errSink, err := cfg.openSinks()
    // error handle

    if cfg.Level == (AtomicLevel{}) {
    return nil, fmt.Errorf("missing Level")
    }

    log := New(
    zapcore.NewCore(enc, sink, cfg.Level),
    cfg.buildOptions(errSink)...,
    )
    if len(opts) > 0 {
    log = log.WithOptions(opts...)
    }
    return log, nil
    }
  • zapcore.NewCore创建了一个ioCore,主要的日志编码和输出逻辑在它的Write方法中实现

    1
    2
    3
    4
    5
    6
    7
    func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core {
    return &ioCore{
    LevelEnabler: enab,
    enc: enc,
    out: ws,
    }
    }

打印日志流程

以Info函数为例

1
2
3
4
5
func (log *Logger) Info(msg string, fields ...Field) {
if ce := log.check(InfoLevel, msg); ce != nil {
ce.Write(fields...)
}
}
  • 执行打印之前会调用Logger的check方法,如果不满足条件就不会创建对象和调用Write方法。

    check方法会先检查level,如果不满足条件就返回nil了。如果满足条件会创建日志entry,并调用core的check,如果core.Check返回了zapcore.CheckedEntry才会继续执行添加其他日志信息操作。实际打印的逻辑发生在zapcore.CheckedEntry的Write方法里面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry{
    // check must always be called directly by a method in the Logger interface
    // (e.g., Check, Info, Fatal).
    const callerSkipOffset = 2

    // Check the level first to reduce the cost of disabled log calls.
    // Since Panic and higher may exit, we skip the optimization for those levels.
    if lvl < zapcore.DPanicLevel && !log.core.Enabled(lvl) {
    return nil
    }

    // Create basic checked entry thru the core; this will be non-nil if the
    // log message will actually be written somewhere.
    ent := zapcore.Entry{
    LoggerName: log.name,
    Time: time.Now(),
    Level: lvl,
    Message: msg,
    }
    ce := log.core.Check(ent, nil)
    willWrite := ce != nil

    // Set up any required terminal behavior.
    // ...

    // Only do further annotation if we're going to write this message; checked
    // entries that exist only for terminal behavior don't benefit from
    // annotation.
    if !willWrite {
    return ce
    }

    // Thread the error output through to the CheckedEntry.
    ce.ErrorOutput = log.errorOutput
    // add caller

    // add stack

    return ce
    }
  • core接口实现的Check方法
    不同的core实现会执行不同操作,但是要执行core的Write方法需要调用AddCore把自己加入到zapcore.CheckedEntry的core列表里面。
    以ioCore为例

    1
    2
    3
    4
    5
    6
    func (c *ioCore) Check(ent Entry, ce *CheckedEntry) *CheckedEntry {
    if c.Enabled(ent.Level) {
    return ce.AddCore(ent, c) // 加入到待执行core列表
    }
    return ce
    }
  • zapcore.CheckedEntry保存了日志原始信息Entry,以及需要执行的core,打印日志后需要执行的动作标识等
    AddCore方法会通过getCheckedEntry方法从sync.Pool中获取CheckedEntry对象,把core加入到待执行列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    type CheckedEntry struct {
    Entry
    ErrorOutput WriteSyncer
    dirty bool // best-effort detection of pool misuse
    should CheckWriteAction
    cores []Core
    }
    func (ce *CheckedEntry) AddCore(ent Entry, core Core) *CheckedEntry {
    if ce == nil {
    ce = getCheckedEntry()
    ce.Entry = ent
    }
    ce.cores = append(ce.cores, core)
    return ce
    }

    执行日志打印的Write方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    func (ce *CheckedEntry) Write(fields ...Field) {
    if ce == nil {
    return
    }

    if ce.dirty {
    // 输出内部错误信息
    return
    }
    ce.dirty = true

    var err error
    // **关键路径**:执行多个core,其他的core例如hook、tee等功能都依赖它来执行
    for i := range ce.cores {
    err = multierr.Append(err, ce.cores[i].Write(ce.Entry, fields))
    }
    if ce.ErrorOutput != nil {
    // 输出内部错误信息
    }

    should, msg := ce.should, ce.Message
    putCheckedEntry(ce)

    // 执行后续动作
    switch should {
    case WriteThenPanic:
    panic(msg)
    case WriteThenFatal:
    exit.Exit()
    }
    }
  • core接口实现的Write方法。具体日志处理逻辑由这个方法实现。

    ioCore

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func (c *ioCore) Write(ent Entry, fields []Field) error {
    // 日志编码,调用具体的编码器实现,结果buf由它分配,使用完需要手动释放
    buf, err := c.enc.EncodeEntry(ent, fields)
    // 处理错误

    // 输出日志,调用具体输出实现
    _, err = c.out.Write(buf.Bytes())
    // 归还buf
    buf.Free()
    // 处理错误

    if ent.Level > ErrorLevel {
    // Since we may be crashing the program, sync the output. Ignore Sync
    // errors, pending a clean solution to issue #370.
    c.Sync()
    }
    return nil
    }
  • encoder接口

    zap定义了所有基础类型和容器对象类型的编码接口,内部有json和console两个实现。
    json编码器从buffer池获取结果buf,通过字符串拼接的方式把日志拼装成json格式,避免了使用encoding/json的反射开销。

  • WriteSyncer接口

    zap实现了输出到文件(stdout/stderr),更高级的功能例如rotation需要自定义WriteSyncer实现然后用它来创建core

日志扩展

Logger对象保存的core接口可以对应多种实现,zap内部扩展功能例如Hook、sampler都是通过保存上一个core形成一个责任链,通过重写Check/Write方法来实现自己的逻辑。
zap提供了工具函数来包装一个core

1
2
3
4
5
func WrapCore(f func(zapcore.Core) zapcore.Core) 	Option {
return optionFunc(func(log *Logger) {
log.core = f(log.core)
})
}

hook在Write方法中调用注册的函数

1
2
3
4
5
6
7
8
9
func (h *hooked) Write(ent Entry, _ []Field) error {
// Since our downstream had a chance to register itself directly with the
// CheckedMessage, we don't need to call it here.
var err error
for i := range h.funcs {
err = multierr.Append(err, h.funcs[i](ent))
}
return err
}

性能

zap在优化性能方法做了以下一些措施

  • 避免创建多余的EntryCheckedEntry

    写日志之前会调用Logger.check,不满足条件不会创建Entry对象
    AddCore方法中才会创建CheckedEntry,而只有真正要执行的core才会调用AddCore方法

  • sync.Pool的使用

    EntryCheckedEntryBuffer等中间对象都通过内存池缓存来减少gc开销

  • 预分配1k的buffer

    zap默认分配1k大小的buffer,避免因为容量不足而扩容。如果应用需要更长的日志,可能需要调整该参数。

  • 强类型参数

    Logger的方法需要传入Field作为上下文信息,Field保存了类型信息,每个对应的类型都实现了编码方法,避免了反射开销。
    缺点是调用的时候需要调用辅助函数来构造Field,略微有点不方便。

  • 自定义编码器

    zap定义了可接受的类型以及对应的json编码实现,通过处理每种类型对应的编码来避免反射开销。

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2015-2024 RivenZoo
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信