zap 自定义日志格式_zap日志框架-源码篇(2)

zap日志框架分了三篇来讲解:使用篇 ,源码篇,性能篇。

流程分析

我们先看一个 logger.Error("logger", zap.String("name", "修华师")) 会走哪些代码

/**
入口方法,参数信息如下:
msg:消息
fields :结构字段信息,可以是0-N个
*/
func (log *Logger) Error(msg string, fields ...Field) {
    //校验是否需要输出 ErrorLevel 日志
    if ce := log.check(ErrorLevel, msg); ce != nil {
        //write 日志
        ce.Write(fields...)
    }
}
​
/**
校验方法,参数信息如下:
lvl:要检验的日志级别,这里传入的是:ErrorLevel
msg :日志消息
*/
func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
    //定义skip,这是为了打印日志caller的信息(通过runtime.Caller(skip))获取
    const callerSkipOffset = 2
​
    //如果lvl<定义的日志级别,则直接返回
    if lvl < zapcore.DPanicLevel && !log.core.Enabled(lvl) {
        return nil
    }
​
    // 封装日志entry,包含了日志的基本信息:time,level,msg,name
    ent := zapcore.Entry{
        LoggerName: log.name,
        Time:       time.Now(),
        Level:      lvl,
        Message:    msg,
    }
    //从对象池中取出CheckedEntry信息,并将core对象和上面的entry对象赋值给CheckedEntry对象,有兴趣的可以点进去看下
    ce := log.core.Check(ent, nil)
    willWrite := ce != nil
​
    // 定义输出后的行为,如果是PanicLevel,FatalLevel 输出完日志后,直接退出
    //如果是DPanicLevel,则只有在定义了development=true的环境下,才会退出
    switch ent.Level {
    case zapcore.PanicLevel:
        ce = ce.Should(ent, zapcore.WriteThenPanic)
    case zapcore.FatalLevel:
        ce = ce.Should(ent, zapcore.WriteThenFatal)
    case zapcore.DPanicLevel:
        if log.development {
            ce = ce.Should(ent, zapcore.WriteThenPanic)
        }
    }
​
    if !willWrite {
        return ce
    }
​
    // 将错误输出赋值给CheckedEntry对象
    ce.ErrorOutput = log.errorOutput
    //是否需要输出调用者信息,如:行号,文件等
    if log.addCaller {
        ce.Entry.Caller = zapcore.NewEntryCaller(runtime.Caller(log.callerSkip + callerSkipOffset))
        if !ce.Entry.Caller.Defined {
            fmt.Fprintf(log.errorOutput, "%v Logger.check error: failed to get callern", time.Now().UTC())
            log.errorOutput.Sync()
        }
    }
    //是否需要将调用者信息记录到stack中,
    if log.addStack.Enabled(ce.Entry.Level) {
        ce.Entry.Stack = Stack("").String
    }
​
    return ce
}
​
​
/**
写日志,参数信息如下:
fields :结构字段信息,可以是0-N个
*/
func (ce *CheckedEntry) Write(fields ...Field) {
    if ce == nil {
        return
    }
​
    //是否是二次输出,如果是,则会警告是Unsafe,直接返回
    //之所以这么做是为了避免从对象池中拿到的CheckedEntry做了多次的write
    if ce.dirty {
        if ce.ErrorOutput != nil {
            fmt.Fprintf(ce.ErrorOutput, "%v Unsafe CheckedEntry re-use near Entry %+v.n", time.Now(), ce.Entry)
            ce.ErrorOutput.Sync()
        }
        return
    }
    ce.dirty = true
​
    var err error
    //开始输出,zap可以允许有多个输出终端,所以会有多个core的情况,关于core的定义,我们在后面会详细讲解
    for i := range ce.cores {
        //Write(ce.Entry, fields),输出entry和fields
        //输出的终端是有用户定义,具体的例子参考上一篇:使用篇(1)
        err = multierr.Append(err, ce.cores[i].Write(ce.Entry, fields))
    }
    //如果发生错误,则输出到ErrorOutput(在定义core时,定义了ouput,默认是os.Stderr)
    if ce.ErrorOutput != nil {
        if err != nil {
            fmt.Fprintf(ce.ErrorOutput, "%v write error: %vn", time.Now(), err)
            ce.ErrorOutput.Sync()
        }
    }
​
    should, msg := ce.should, ce.Message
    //回收CheckedEntry
    putCheckedEntry(ce)
    //判断是否需要退出,should实在上面的 check 方法终端赋值的
    switch should {
    case WriteThenPanic:
        panic(msg)
    case WriteThenFatal:
        exit.Exit()
    }
}
​

上面的代码基本上展示了输出日志的整个流程,还是非常简单的,下面对zap的模块做一些分析,体会一下zap的设计之美。

结构

Level

日志级别的定义,不做过多阐述,需要注意的是,zap特有的日志级别:DPanicLevel,此日志级别在开发环境下(设置了Logger.development=true),会将panic日志打印出来,然后发出panic。但是在非开发环境下,只会打印panic日志,不会退出

​
type Level int8
​
const (
    // DebugLevel logs are typically voluminous, and are usually disabled in
    // production.
    DebugLevel Level = iota - 1
    // InfoLevel is the default logging priority.
    InfoLevel
    // WarnLevel logs are more important than Info, but don't need individual
    // human review.
    WarnLevel
    // ErrorLevel logs are high-priority. If an application is running smoothly,
    // it shouldn't generate any error-level logs.
    ErrorLevel
    // DPanicLevel logs are particularly important errors. In development the
    // logger panics after writing the message.
    DPanicLevel
    // PanicLevel logs a message, then panics.
    PanicLevel
    // FatalLevel logs a message, then calls os.Exit(1).
    FatalLevel
​
    _minLevel = DebugLevel
    _maxLevel = FatalLevel
)

Logger

Logger是一个结构体,定义了与输出相关的基本信息,比如:name,stack,core等,我们可以看到这些属性都是不对外公开的,所以不能直接初始化结构体。zap为我们提供了NewBuild两种方式来初始化Logger。除了core以外,其他的都可以通过Option来设置,这里先留个印象,后面会看到如何设置。

type Logger struct {
    //core:定义了输出日志核心接口
    core zapcore.Core
    development bool
    name        string
    //错误输出终端,注意区别于zapcore中的输出,这里一般是指做运行过程中,发生错误记录日志(如:参数错误,未定义错误等),默认是os.Stderr
    errorOutput zapcore.WriteSyncer
    //是否输出调用者的信息
    addCaller bool
    //需要记录stack信息的日志级别
    addStack  zapcore.LevelEnabler
    //调用者的层级:用于指定记录哪个调用者信息
    callerSkip int
}

zap为我们提供了几个默认的Logger初始化方法

方式一:自定义的new方法,我们我们 使用篇就是使用的这个方法,代码很简单。我们再说上文提到的errorOutput,其输出的终端就是os.Stderr

func New(core zapcore.Core, options ...Option) *Logger {
    if core == nil {
        return NewNop()
    }
    log := &Logger{
        core:        core,
        errorOutput: zapcore.Lock(os.Stderr),
        addStack:    zapcore.FatalLevel + 1,
    }
    return log.WithOptions(options...)
}

需要注意的参数里面有个Option数组,我们看下Option接口的定义,其中自定义的函数类型:optionFunc 实现了该接口,这种设计思路值得我们借鉴,类似于java中的匿名函数,可以有效的避免代码的冗余,进一步的凸显简洁之美,同时避免了上面的func New(core zapcore.Core, options ...Option) *Logger,一大串的参数定义(如果不是Option,我们可能会定义出这样的函数 func New(core zapcore.Core, xx int,xx string,xx Caller,...))。

以下代码在: options.go文件,文件中还提供了几个Option方法
type Option interface {
    apply(*Logger)
}
// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*Logger)
​
func (f optionFunc) apply(log *Logger) {
    f(log)
}
​

我们看下zap提供几个Option实现,使用方法就:将其传到的上面的New方法中,就可以完成Logger的定义,这些有一个共同特点:设置Logger的基本属性

//设置成开发模式
func Development() Option {
    return optionFunc(func(log *Logger) {
        log.development = true
    })
}
//自定义错误输出路径
func ErrorOutput(w zapcore.WriteSyncer) Option {
    return optionFunc(func(log *Logger) {
        log.errorOutput = w
    })
}
//Logger的结构化字段,每条日志都会打印这些Filed信息
func Fields(fs ...Field) Option {
    return optionFunc(func(log *Logger) {
        log.core = log.core.With(fs)
    })
}
//日志添加调用者信息
func AddCaller() Option {
    return WithCaller(true)
}
func WithCaller(enabled bool) Option {
    return optionFunc(func(log *Logger) {
        log.addCaller = enabled
    })
}
​
//设置skip,用户runtime.Caller的参数
func AddCallerSkip(skip int) Option {
    return optionFunc(func(log *Logger) {
        log.callerSkip += skip
    })
}
​
//设置stack
func AddStacktrace(lvl zapcore.LevelEnabler) Option {
    return optionFunc(func(log *Logger) {
        log.addStack = lvl
    })
}

zap还为我们添加了hook,让我们在每次打印日志的时候,可以调用hook方法:比如可以统计打印日志的次数、统计打印字段等

func Hooks(hooks ...func(zapcore.Entry) error) Option {
    return optionFunc(func(log *Logger) {
        log.core = zapcore.RegisterHooks(log.core, hooks...)
    })
}

方式二:构造器模式,为我们提供了开箱即用的Logger对象,并且区分了不同环境

//开发环境下的Logger
func NewDevelopment(options ...Option) (*Logger, error) {
    return NewDevelopmentConfig().Build(options...)
}
//生产环境下的Logger
func NewProduction(options ...Option) (*Logger, error) {
    return NewProductionConfig().Build(options...)
}
//测试环境下的Logger
func NewExample(options ...Option) *Logger {
    encoderCfg := zapcore.EncoderConfig{
        MessageKey:     "msg",
        LevelKey:       "level",
        NameKey:        "logger",
        EncodeLevel:    zapcore.LowercaseLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.StringDurationEncoder,
    }
    core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), os.Stdout, DebugLevel)
    return New(core).WithOptions(options...)
}

不同的构造方式,唯一不同的就是Config,我们来看下Config的定义,具体NewDevelopmentConfig()NewProductionConfig(),的实现,可自行看源码

type Config struct {
    Level AtomicLevel `json:"level" yaml:"level"`
    Development bool `json:"development" yaml:"development"`
    DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    //取值:json,console,代表两种输出格式
    Encoding string `json:"encoding" yaml:"encoding"`
    //定义了输出样式
    EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    //日志的输出路径
    OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    //错误日志的输入路径
    ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    //初始化的Fields,每行日志都会爱上这些Field
    InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

zapcore.EncoderConfig:定义了输出的样式,比如Key,调用者样式等

type EncoderConfig struct {
    //*Key:设置的是在结构化输出时,value对应的key
    MessageKey    string `json:"messageKey" yaml:"messageKey"`
    LevelKey      string `json:"levelKey" yaml:"levelKey"`
    TimeKey       string `json:"timeKey" yaml:"timeKey"`
    NameKey       string `json:"nameKey" yaml:"nameKey"`
    CallerKey     string `json:"callerKey" yaml:"callerKey"`
    StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
    //日志的结束符
    LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
    //Level的输出样式,比如 大小写,颜色等
    EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
    //日志时间的输出样式
    EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
    //消耗时间的输出样式
    EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
    //Caller的输出样式,比如 全名称,短名称
    EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
    // Unlike the other primitive type encoders, EncodeName is optional. The
    // zero value falls back to FullNameEncoder.
    EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
}

可以看到,上面都应了json,yaml的tag,也就是说我们可以通过配置文件来初始化这些Config,对开发者非常的友好。

zapcore

zapcore是一个接口,之所以定义成接口,是因为zap需要提供不同的实现,做到接口与实现解耦,充分体现了面向接口编程的设计思路

我们先看这个接口定义:

type Core interface {
    //level接口:是用来根据日志级别判断日志是否应该输出
    LevelEnabler
​
    //添加结构化字段的方法
    With([]Field) Core
    
    //从对象池取出CheckedEntry对象,并关联输出实体entry和core信息
    Check(Entry, *CheckedEntry) *CheckedEntry
    //写入日志的方法
    Write(Entry, []Field) error
    //刷新到终端的方法
    Sync() error
}
​

在zap包中,我们可以看到core有如下的实现:

966a901c1727f084360ea702fbca888f.png

下面的例子我们使用的是:mutiCoreioCore

//声明一个mutiCore,输出到多个终端
core := zapcore.NewTee(
    // 每个终端都是由ioCore来实现
    zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(infoWriter), infoLevel),
    zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(warnWriter), warnLevel),
    zapcore.NewCore(zapcore.NewJSONEncoder(config), zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)), logLevel),
)

接下来我们对每个Core做一个简单的分析

ioCore

底层实现,其他的core实现基本上都依赖于ioCore,我们先来看ioCore的结构体定义

type ioCore struct {
    LevelEnabler
    enc Encoder
    out WriteSyncer
}
  • LevelEnabler 继承 Enabled(Level) bool方法。
  • enc Encoder定义是日志的输出格式:
    • zapcore.NewConsoleEncoder 非结构化日志
    • zapcore.NewJSONEncoder 结构化日志
  • 为了线程问题,我们在里面可以看到大量的clone方法,这一点值得我们借鉴
在运行过程中,我们可能会对 loggercore对象做一些With操作,如果不做clone,则会改变其他的调用行为,这就属于脏数据
func (c *ioCore) With(fields []Field) Core {
    clone := c.clone()
    addFields(clone.enc, fields)
    return clone
}
func (c *ioCore) clone() *ioCore {
    return &ioCore{
        LevelEnabler: c.LevelEnabler,
        enc:          c.enc.Clone(),
        out:          c.out,
    }
}

hooked

hooked,顾名思义,就是为我们提供钩子方法,前面有提到过,我们先看他的定义

type hooked struct {
    Core
    funcs []func(Entry) error
}
  • Core :组合了其他的Core,比如:multiCore,ioCore等
  • funcs: 钩子方法,在Write时,会调用该方法。源代码如下:
func (h *hooked) Write(ent Entry, _ []Field) error {
    var err error
    for i := range h.funcs {
        err = multierr.Append(err, h.funcs[i](ent))
    }
    return err
}

我们看看如何使用这个hook

logger = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.WarnLevel), zap.Hooks(func(entry zapcore.Entry) error {//定义钩子函数
        fmt.Println("hooked called")
        return nil
    })) 
//打印日志
logger.Info("logger", zap.String("name", "修华师"))

打印的结果是

{"level":"INFO","ts":"2020-05-18 15:20:56","file":"test/main.go:35","msg":"logger","name":"修华师"}
hooked called

multiCore

这是一个组合Core,他的定义就是:

type multiCore []Core

这样就实现了可以往多个终端打印,我们看multiCore的初始化方法,很简单

func NewTee(cores ...Core) Core {
    switch len(cores) {
    case 0:
        return NewNopCore()
    case 1:
        return cores[0]
    default:
        return multiCore(cores)
    }
}

在看他的Write方法,就是循环Core,然后Write

func (mc multiCore) Write(ent Entry, fields []Field) error {
    var err error
    for i := range mc {
        err = multierr.Append(err, mc[i].Write(ent, fields))
    }
    return err
}

zap还为我们提供了如下的Core,这里就一一介绍了,读者可以自己看,都非常的简单

  • nopCore:什么也不做
  • countingCore:看名字就很清楚了,只负责计数

encoder

json的编码器,为了提升性能,zap自己通过字符串拼接的方式来组装json字符串,可见其用工很足。具体可以看zapcore.EncodeEntry方法,代码很长,这里就不贴了,关键是很容易懂。

结语

整个源码读下来,会发现zap使用了大量的对象组合+接口,从解耦的角度上来说,这是非常值得我们学习的。最后,贴一张zap的结构图,方便我们加深理解。

97176c9d4d417df72b9df59df59a3f4a.png

这篇已经很长很长了,我知道读者的时间很宝贵,所以先到这里,下次我们再从性能的角度来分析zap,看看zap为了性能做了哪些功课。

还有不太了解的地方,欢迎留言,我们一起成长!

相关阅读

zap日志框架-使用篇(1)

zap日志框架-性能篇(3)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值