首页 > 其他分享 >(转)一文带你由浅入深地解读 Go Zap 的高性能

(转)一文带你由浅入深地解读 Go Zap 的高性能

时间:2023-04-21 17:11:44浏览次数:33  
标签:由浅入深 zapcore ce Zap Go 日志 logger zap op

原文:https://blog.csdn.net/EDDYCJY/article/details/117970643

简介

zap 是什么?

⚡ZAP[1] 是uber 开源的提供快速,结构化,高性能的日志记录包。

zap 高性能体现在哪里?

在介绍zap包的优化部分之前,让我们看下zap日志库的工作流程图

大多数日志库提供的方式是基于反射的序列化和字符串格式化,这种方式代价高昂,而 Zap 采取不同的方法。

  • 避免 interface{} 使用强类型设计

  • 封装强类型,无反射

  • 使用零分配内存的 JSON 编码器,尽可能避免序列化开销,它比其他结构化日志包快 4 - 10 倍。

  1.   logger.Info("failed to fetch URL",
  2.     zap.String("url", "https://baidu.com"),
  3.     zap.Int("attempt", 3),
  4.     zap.Duration("backoff", time.Second),
  5.   )
  • 使用 sync.Pool 以避免记录消息时的内存分配

详情在下文 zapcore 模块介绍。

Example

安装

go get -u go.uber.org/zap

Zap 提供了两种类型的 logger

  • SugaredLogger

  • Logger

在性能良好但不是关键的情况下,使用 SugaredLogger,它比其他结构化的日志包快 4-10 倍,并且支持结构化和 printf 风格的APIs。

例一 调用 NewProduction 创建logger对象

  1.   func TestSugar(t *testing.T) {
  2.    logger, _ := zap.NewProduction()
  3.    // 默认 logger 不缓冲。
  4.    // 但由于底层 api 允许缓冲,所以在进程退出之前调用 Sync 是一个好习惯。
  5.    defer logger.Sync()
  6.    sugar := logger.Sugar()
  7.    sugar.Infof("Failed to fetch URL: %s", "https://baidu.com")
  8.   }

对性能和类型安全要求严格的情况下,可以使用 Logger ,它甚至比前者SugaredLogger更快,内存分配次数也更少,但它仅支持强类型的结构化日志记录。

例二 调用 NewDevelopment 创建logger对象

  1.   func TestLogger(t *testing.T) {
  2.    logger, _ := zap.NewDevelopment()
  3.    defer logger.Sync()
  4.    logger.Info("failed to fetch URL",
  5.     // 强类型字段
  6.     zap.String("url", "https://baidu.com"),
  7.     zap.Int("attempt", 3),
  8.     zap.Duration("backoff", time.Second),
  9.    )
  10.   }

不需要为整个应用程序决定选择使用 Logger 还是 SugaredLogger ,两者之间都可以轻松转换。

例三 Logger 与 SugaredLogger 相互转换

  1.   // 创建 logger
  2.   logger := zap.NewExample()
  3.   defer logger.Sync()
  4.    
  5.   // 转换 SugaredLogger
  6.   sugar := logger.Sugar()
  7.   // 转换 logger
  8.   plain := sugar.Desugar()

例四 自定义格式

自定义一个日志消息格式,带着问题看下列代码。

  1. debug 级别的日志打印到控制台了吗?

  2. 最后的 error 会打印到控制台吗 ?

  1.   package main
  2.    
  3.   import (
  4.    "os"
  5.    
  6.    "go.uber.org/zap"
  7.    "go.uber.org/zap/zapcore"
  8.   )
  9.    
  10.   func NewCustomEncoderConfig() zapcore.EncoderConfig {
  11.    return zapcore.EncoderConfig{
  12.     TimeKey:        "ts",
  13.     LevelKey:       "level",
  14.     NameKey:        "logger",
  15.     CallerKey:      "caller",
  16.     FunctionKey:    zapcore.OmitKey,
  17.     MessageKey:     "msg",
  18.     StacktraceKey:  "stacktrace",
  19.     LineEnding:     zapcore.DefaultLineEnding,
  20.     EncodeLevel:    zapcore.CapitalColorLevelEncoder,
  21.     EncodeTime:     zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05"),
  22.     EncodeDuration: zapcore.SecondsDurationEncoder,
  23.     EncodeCaller:   zapcore.ShortCallerEncoder,
  24.    }
  25.   }
  26.    
  27.   func main() {
  28.    atom := zap.NewAtomicLevelAt(zap.DebugLevel)
  29.    core := zapcore.NewCore(
  30.     zapcore.NewConsoleEncoder(NewCustomEncoderConfig()),
  31.     zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
  32.     atom,
  33.    )
  34.    logger := zap.New(core, zap.AddCaller(), zap.Development())
  35.    defer logger.Sync()
  36.    
  37.    // 配置 zap 包的全局变量
  38.    zap.ReplaceGlobals(logger)
  39.    
  40.    // 运行时安全地更改 logger 日记级别
  41.    atom.SetLevel(zap.InfoLevel)
  42.    sugar := logger.Sugar()
  43.    // 问题 1: debug 级别的日志打印到控制台了吗?
  44.    sugar.Debug("debug")
  45.    sugar.Info("info")
  46.    sugar.Warn("warn")
  47.    sugar.DPanic("dPanic")
  48.    // 问题 2: 最后的 error 会打印到控制台吗?
  49.    sugar.Error("error")
  50.   }

结果见下图

image-20210525201656456

问题 1:

没有打印。AtomicLevel 是原子性可更改的动态日志级别,通过调用 atom.SetLevel 更改日志级别为 infoLevel 。

问题 2:

没有打印。zap.Development() 启用了开发模式,在开发模式下 DPanic 函数会引发 panic,所以最后的 error 不会打印到控制台。

源码分析

此次源码分析基于 Zap 1.16

zap概览

上图仅表示 zap 可调用两种 logger,没有表达 Logger 与 SugaredLogger 的关系,继续往下看,你会更理解。

Logger

logger 提供快速,分级,结构化的日志记录。所有的方法都是安全的,内存分配很重要,因此它的 API 有意偏向于性能和类型安全。

[email protected] - logger.go

  1.   type Logger struct {
  2.     // 实现编码和输出的接口
  3.    core zapcore.Core  
  4.     // 记录器开发模式,DPanic 等级将记录 panic
  5.    development bool
  6.     // 开启记录调用者的行号和函数名
  7.    addCaller   bool  
  8.     // 致命日志采取的操作,默认写入日志后 os.Exit()
  9.     onFatal     zapcore.CheckWriteAction 
  10.    name        string 
  11.     // 设置记录器生成的错误目的地
  12.    errorOutput zapcore.WriteSyncer  
  13.     // 记录 >= 该日志等级的堆栈追踪
  14.    addStack zapcore.LevelEnabler 
  15.     // 避免记录器认为封装函数为调用方
  16.    callerSkip int 
  17.     // 默认为系统时间 
  18.    clock Clock  
  19.   }

在 Example 中分别使用了 NewProduction 和 NewDevelopment ,接下来以这两个函数开始分析。下图表示 A 函数调用了 B 函数,其中箭头表示函数调用关系。图中函数都会分析到。

NewProduction

从下面代码中可以看出,此函数是对 NewProductionConfig().Build(...) 封装的快捷方式。

[email protected] - logger.go

  1.   func NewProduction(options ...Option) (*Logger, error) {
  2.    return NewProductionConfig().Build(options...)
  3.   }

NewProductionConfig

在 InfoLevel 及更高级别上启用了日志记录。它使用 JSON 编码器,写入 stderr,启用采样。

[email protected] - config.go

  1.   func NewProductionConfig() Config {
  2.    return Config{
  3.       // info 日志级别
  4.     Level:       NewAtomicLevelAt(InfoLevel),
  5.       // 非开发模式
  6.     Development: false,
  7.       // 采样设置
  8.     Sampling: &SamplingConfig{
  9.      Initial:    100, // 相同日志级别下相同内容每秒日志输出数量
  10.      Thereafter: 100, // 超过该数量,才会再次输出
  11.     },
  12.       // JSON 编码器
  13.     Encoding:         "json",
  14.       // 后面介绍
  15.     EncoderConfig:    NewProductionEncoderConfig(),
  16.       // 输出到 stderr
  17.     OutputPaths:      []string{"stderr"},
  18.     ErrorOutputPaths: []string{"stderr"},
  19.    }
  20.   }

Config 结构体

通过 Config 可以设置通用的配置项。

[email protected] - config.go

  1.   type Config struct {
  2.    // 日志级别
  3.    Level AtomicLevel `json:"level" yaml:"level"`
  4.    // 开发模式
  5.    Development bool `json:"development" yaml:"development"`
  6.    // 停止使用调用方的函数和行号
  7.    DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
  8.    // 完全停止使用堆栈跟踪,默认为  `>=WarnLevel` 使用堆栈跟踪
  9.    DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
  10.    // 采样设置策略
  11.    Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
  12.    // 记录器的编码,有效值为 'json' 和 'console' 以及通过 `RegisterEncoder` 注册的有效编码
  13.    Encoding string `json:"encoding" yaml:"encoding"`
  14.    // 编码器选项
  15.    EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
  16.    // 日志的输出路径
  17.    OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
  18.    // zap 内部错误的输出路径
  19.    ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
  20.    // 添加到根记录器的字段的集合
  21.    InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
  22.   }

NewDevelopment

从下面代码中可以看出,此函数是对 NewDevelopmentConfig().Build(...) 封装的快捷方式

[email protected] - logger.go

  1.   func NewDevelopment(options ...Option) (*Logger, error) {
  2.    return NewDevelopmentConfig().Build(options...)
  3.   }

NewDevelopmentConfig

此函数在 DebugLevel 及更高版本上启用日志记录,它使用 console 编码器,写入 stderr,禁用采样。

[email protected] - config.go

  1.   func NewDevelopmentConfig() Config {
  2.    return Config{
  3.       // debug 等级
  4.     Level:            NewAtomicLevelAt(DebugLevel),
  5.       // 开发模式
  6.     Development:      true,
  7.       // console 编码器
  8.     Encoding:         "console",
  9.     EncoderConfig:    NewDevelopmentEncoderConfig(),
  10.       // 输出到 stderr
  11.     OutputPaths:      []string{"stderr"},
  12.     ErrorOutputPaths: []string{"stderr"},
  13.    }
  14.   }

NewProductionEncoderConfig 和 NewDevelopmentEncoderConfig 都是返回编码器配置。

[email protected] - config.go

  1.   type EncoderConfig struct {
  2.    // 设置 编码为 JSON 时的 KEY
  3.     // 如果为空,则省略
  4.    MessageKey    string `json:"messageKey" yaml:"messageKey"`
  5.    LevelKey      string `json:"levelKey" yaml:"levelKey"`
  6.    TimeKey       string `json:"timeKey" yaml:"timeKey"`
  7.    NameKey       string `json:"nameKey" yaml:"nameKey"`
  8.    CallerKey     string `json:"callerKey" yaml:"callerKey"`
  9.    FunctionKey   string `json:"functionKey" yaml:"functionKey"`
  10.    StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
  11.     // 配置行分隔符
  12.    LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
  13.    // 配置常见复杂类型的基本表示形式。
  14.    EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
  15.    EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
  16.    EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
  17.    EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
  18.    // 日志名称,此参数可选
  19.    EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
  20.    // 配置 console 编码器使用的字段分隔符,默认 tab
  21.    ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
  22.   }

NewProductionEncoderConfig

[email protected] - config.go

  1.   func NewProductionEncoderConfig() zapcore.EncoderConfig {
  2.    return zapcore.EncoderConfig{
  3.     TimeKey:        "ts",
  4.     LevelKey:       "level",
  5.     NameKey:        "logger",
  6.     CallerKey:      "caller",
  7.     FunctionKey:    zapcore.OmitKey,
  8.     MessageKey:     "msg",
  9.     StacktraceKey:  "stacktrace",
  10.       // 默认换行符 \n
  11.     LineEnding:     zapcore.DefaultLineEnding,
  12.       // 日志等级序列为小写字符串,如:InfoLevel被序列化为 "info"
  13.     EncodeLevel:    zapcore.LowercaseLevelEncoder,
  14.       // 时间序列化成浮点秒数
  15.     EncodeTime:     zapcore.EpochTimeEncoder,
  16.       // 时间序列化,Duration为经过的浮点秒数
  17.     EncodeDuration: zapcore.SecondsDurationEncoder,
  18.       // 以 包名/文件名:行数 格式序列化
  19.     EncodeCaller:   zapcore.ShortCallerEncoder,
  20.    }
  21.   }

该配置会输出如下结果,此结果出处参见 Example 中的例一

{"level":"info","ts":1620367988.461055,"caller":"test/use_test.go:24","msg":"Failed to fetch URL: https://baidu.com"}

NewDevelopmentEncoderConfig

[email protected] - config.go

  1.   func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
  2.    return zapcore.EncoderConfig{
  3.     // keys 值可以是任意非空的值
  4.     TimeKey:        "T",
  5.     LevelKey:       "L",
  6.     NameKey:        "N",
  7.     CallerKey:      "C",
  8.     FunctionKey:    zapcore.OmitKey,
  9.     MessageKey:     "M",
  10.     StacktraceKey:  "S",
  11.        // 默认换行符 \n
  12.     LineEnding:     zapcore.DefaultLineEnding,
  13.       // 日志等级序列为大写字符串,如:InfoLevel被序列化为 "INFO"
  14.     EncodeLevel:    zapcore.CapitalLevelEncoder,
  15.       // 时间格式化为  ISO8601 格式
  16.     EncodeTime:     zapcore.ISO8601TimeEncoder,
  17.     EncodeDuration: zapcore.StringDurationEncoder,
  18.       // // 以 包名/文件名:行数 格式序列化
  19.     EncodeCaller:   zapcore.ShortCallerEncoder,
  20.    }
  21.   }

该配置会输出如下结果,此结果出处参见 Example 中的例二

2021-05-07T14:14:12.434+0800 INFO test/use_test.go:31 failed to fetch URL {"url": "https://baidu.com", "attempt": 3, "backoff": "1s"}

NewProductionConfig 和 NewDevelopmentConfig 返回 config 调用 Build 函数返回 logger,接下来我们看看这个函数。

[email protected] - config.go

  1.   func (cfg Config) Build(opts ...Option) (*Logger, error) {
  2.     enc, err := cfg.buildEncoder()
  3.    if err != nil {
  4.     return nil, err
  5.    }
  6.    
  7.    sink, errSink, err := cfg.openSinks()
  8.    if err != nil {
  9.     return nil, err
  10.    }
  11.    
  12.    if cfg.Level == (AtomicLevel{}) {
  13.     return nil, fmt.Errorf("missing Level")
  14.    }
  15.    
  16.    log := New(
  17.     zapcore.NewCore(enc, sink, cfg.Level),
  18.     cfg.buildOptions(errSink)...,
  19.    )
  20.    if len(opts) > 0 {
  21.     log = log.WithOptions(opts...)
  22.    }
  23.    return log, nil
  24.   }

从上面的代码中,通过解析 config 的参数,调用 New 方法来创建 Logger。在 Example 中例四,就是调用 New 方法来自定义 Logger。

SugaredLogger

Logger 作为 SugaredLogger 的属性,这个封装优点在于不是很在乎性能的情况下,可以快速调用Logger。所以名字为加了糖的 Logger。

[email protected] - logger.go

  1.   type SugaredLogger struct {
  2.    base *Logger
  3.   }
  1.   zap.ReplaceGlobals(logger)   // 重新配置全局变量
  2.   zap.S().Info("SugaredLogger")   // S 返回全局 SugaredLogger
  3.   zap.L().Info("logger")      // L 返回全局 logger

Logger不同,SugaredLogger不强制日志结构化。所以对于每个日志级别,都提供了三种方法。

level

[email protected] - sugar.go

以 info 级别为例,相关的三种方法。

  1.   // Info 使用 fmt.Sprint 构造和记录消息。
  2.   func (s *SugaredLogger) Info(args ...interface{}) {
  3.    s.log(InfoLevel, "", args, nil)
  4.   }
  5.    
  6.   // Infof 使用 fmt.Sprintf 记录模板消息。
  7.   func (s *SugaredLogger) Infof(template string, args ...interface{}) {
  8.    s.log(InfoLevel, template, args, nil)
  9.   }
  10.    
  11.   // Infow 记录带有其他上下文的消息
  12.   func (s *SugaredLogger) Infow(msg string, keysAndValues ...interface{}) {
  13.    s.log(InfoLevel, msg, nil, keysAndValues)
  14.   }

在 sugar.Infof("...") 打上断点,从这开始追踪源码。

image-20210519111252185

在调试代码之前,先给大家看一下SugaredLogger 的  Infof 函数的调用的大致工作流,其中不涉及采样等。

infof工作流程

Info , InfofInfow 三个函数都调用了 log 函数,log 函数代码如下

[email protected] - sugar.go

  1.   func (s *SugaredLogger) log(lvl zapcore.Level, template string, fmtArgs []interface{}, context []interface{}) {
  2.    // 判断是否启用的日志级别
  3.    if lvl < DPanicLevel && !s.base.Core().Enabled(lvl) {
  4.     return
  5.    }
  6.    // 将参数合并到语句中
  7.    msg := getMessage(template, fmtArgs)
  8.     // Check 可以帮助避免分配一个分片来保存字段。
  9.    if ce := s.base.Check(lvl, msg); ce != nil {
  10.     ce.Write(s.sweetenFields(context)...)
  11.    }
  12.   }

函数的第一个参数 InfoLevel 是日志级别,其源码如下

[email protected] - zapcore/level.go

  1.   const (
  2.    // Debug 应是大量的,且通常在生产状态禁用.
  3.    DebugLevel = zapcore.DebugLevel
  4.    // Info 是默认的记录优先级.
  5.    InfoLevel = zapcore.InfoLevel
  6.    // Warn 比 info 更重要.
  7.    WarnLevel = zapcore.WarnLevel
  8.    // Error 是高优先级的,如果程序顺利不应该产生任何 err 级别日志.
  9.    ErrorLevel = zapcore.ErrorLevel
  10.    // DPanic 特别重大的错误,在开发模式下引起 panic. 
  11.    DPanicLevel = zapcore.DPanicLevel
  12.    // Panic 记录消息后调用 panic.
  13.    PanicLevel = zapcore.PanicLevel
  14.    // Fatal 记录消息后调用 os.Exit(1).
  15.    FatalLevel = zapcore.FatalLevel
  16.   )

getMessage 函数处理 template 和 fmtArgs 参数,主要为不同的参数选择最合适的方式拼接消息

[email protected] - sugar.go

  1.   func getMessage(template string, fmtArgs []interface{}) string {
  2.     // 没有参数直接返回 template
  3.    if len(fmtArgs) == 0 {
  4.     return template
  5.    }
  6.    
  7.     // 此处调用 Sprintf 会使用反射
  8.    if template != "" {
  9.     return fmt.Sprintf(template, fmtArgs...)
  10.    }
  11.    
  12.     // 消息为空并且有一个参数,返回该参数
  13.    if len(fmtArgs) == 1 {
  14.     if str, ok := fmtArgs[0].(string); ok {
  15.      return str
  16.     }
  17.    }
  18.     // 返回所有 fmtArgs
  19.    return fmt.Sprint(fmtArgs...)
  20.   }

关于 s.base.Check ,这就需要介绍zapcore ,下面分析相关模块。

zapcore

zapcore包 定义并实现了构建 zap 的低级接口。通过提供这些接口的替代实现,外部包可以扩展 zap 的功能。

[email protected] - zapcore/core.go

  1.   // Core 是一个最小的、快速的记录器接口。
  2.   type Core interface {
  3.     // 接口,决定一个日志等级是否启用
  4.    LevelEnabler
  5.    // 向 core 添加核心上下文
  6.    With([]Field) Core
  7.    // 检查是否应记录提供的条目
  8.     // 在调用 write 之前必须先调用 Check
  9.    Check(Entry, *CheckedEntry) *CheckedEntry
  10.    // 写入日志
  11.    Write(Entry, []Field) error
  12.     // 同步刷新缓存日志(如果有)
  13.    Sync() error
  14.   }

Check 函数有两个入参。第一个参数表示一条完整的日志消息,第二个参数为 nil 时会从 sync.Pool 创建的池中取出*CheckedEntry对象复用,避免重新分配内存。该函数内部调用 AddCore 实现获取 *CheckedEntry对象,最后调用 Write 写入日志消息。

相关代码全部贴在下面,更多介绍请看代码中的注释。

[email protected] - zapcore/entry.go

  1.   // 一个 entry 表示一个完整的日志消息
  2.   type Entry struct {
  3.    Level      Level
  4.    Time       time.Time
  5.    LoggerName string
  6.    Message    string
  7.    Caller     EntryCaller
  8.    Stack      string
  9.   }
  1.   // 使用 sync.Pool 复用临时对象
  2.   var (
  3.    _cePool = sync.Pool{New: func() interface{} {
  4.     return &CheckedEntry{
  5.      cores: make([]Core, 4),
  6.     }
  7.    }}
  8.   )
  9.    
  10.   // 从池中取出 CheckedEntry 并初始化值
  11.   func getCheckedEntry() *CheckedEntry {
  12.    ce := _cePool.Get().(*CheckedEntry)
  13.    ce.reset()
  14.    return ce
  15.   }
  16.    
  17.    
  18.   // CheckedEntry 是 enter 和 cores 集合。
  19.   type CheckedEntry struct {
  20.    Entry
  21.    ErrorOutput WriteSyncer
  22.    dirty       bool  // 用于检测是否重复使用对象
  23.    should      CheckWriteAction // 结束程序的动作
  24.    cores       []Core
  25.   }
  26.    
  27.   // 重置对象
  28.   func (ce *CheckedEntry) reset() {
  29.    ce.Entry = Entry{}
  30.    ce.ErrorOutput = nil
  31.    ce.dirty = false
  32.    ce.should = WriteThenNoop
  33.    for i := range ce.cores {
  34.     // 不要保留对 core 的引用!!
  35.     ce.cores[i] = nil
  36.    }
  37.    ce.cores = ce.cores[:0]
  38.   }
  39.    
  40.   // 将 entry 写入存储的 cores
  41.   // 最后将 CheckedEntry 添加到池中
  42.   func (ce *CheckedEntry) Write(fields ...Field) {
  43.    if ce == nil {
  44.     return
  45.    }
  46.    
  47.    if ce.dirty {
  48.     if ce.ErrorOutput != nil {
  49.         // 检查 CheckedEntry 的不安全重复使用
  50.      fmt.Fprintf(ce.ErrorOutput, "%v Unsafe CheckedEntry re-use near Entry %+v.\n", ce.Time, ce.Entry)
  51.      ce.ErrorOutput.Sync()
  52.     }
  53.     return
  54.    }
  55.    ce.dirty = true
  56.    
  57.    var err error
  58.     // 写入日志消息
  59.    for i := range ce.cores {
  60.     err = multierr.Append(err, ce.cores[i].Write(ce.Entry, fields))
  61.    }
  62.     // 处理内部发生的错误
  63.    if ce.ErrorOutput != nil {
  64.     if err != nil {
  65.      fmt.Fprintf(ce.ErrorOutput, "%v write error: %v\n", ce.Time, err)
  66.      ce.ErrorOutput.Sync()
  67.     }
  68.    }
  69.    
  70.    should, msg := ce.should, ce.Message
  71.     // 将 CheckedEntry 添加到池中,下次复用
  72.    putCheckedEntry(ce)
  73.    
  74.     // 判断是否需要 panic 或其它方式终止程序..
  75.    switch should {
  76.    case WriteThenPanic:
  77.     panic(msg)
  78.    case WriteThenFatal:
  79.     exit.Exit()
  80.    case WriteThenGoexit:
  81.     runtime.Goexit()
  82.    }
  83.   }
  84.    
  85.   func (ce *CheckedEntry) AddCore(ent Entry, core Core) *CheckedEntry {
  86.    if ce == nil {
  87.       // 从池中取 CheckedEntry,减少内存分配
  88.     ce = getCheckedEntry()
  89.     ce.Entry = ent
  90.    }
  91.    ce.cores = append(ce.cores, core)
  92.    return ce
  93.   }

Doc

https://pkg.go.dev/go.uber.org/zap

QA

设计问题

为什么要在Logger性能上花费这么多精力呢?

当然,大多数应用程序不会注意到Logger慢的影响:因为它们每次操作会需要几十或几百毫秒,所以额外的几毫秒很无关紧要。

另一方面,为什么不使用结构化日志快速开发呢?与其他日志包相比SugaredLogger的使用并不难,Logger使结构化记录在对性能要求严格的环境中成为可能。在 Go 微服务的架构体系中,使每个应用程序甚至稍微更有效地加速执行。

为什么没有LoggerSugaredLogger接口?

不像熟悉的io.Writerhttp.HandlerLoggerSugaredLogger接口将包括很多方法。正如 Rob Pike 谚语指出[2]的,"The bigger the interface, the weaker the abstraction"(接口越大,抽象越弱)。接口也是严格的,任何更改都需要发布一个新的主版本,因为它打破了所有第三方实现。

LoggerSugaredLogger成为具体类型并不会牺牲太多抽象,而且它允许我们在不引入破坏性更改的情况下添加方法。您的应用程序应该定义并依赖只包含您使用的方法的接口。

为什么我的一些日志会丢失?

在启用抽样时,通过zap有意地删除日志。生产配置(如NewProductionConfig()返回的那样)支持抽样,这将导致在一秒钟内对重复日志进行抽样。有关为什么启用抽样的更多详细信息,请参见"为什么使用示例应用日志"中启用采样.

为什么要使用示例应用程序日志?

应用程序经常会遇到错误,无论是因为错误还是因为用户使用错误。记录错误日志通常是一个好主意,但它很容易使这种糟糕的情况变得更糟:不仅您的应用程序应对大量错误,它还花费额外的CPU周期和I/O记录这些错误日志。由于写入通常是序列化的,因此在最需要时,logger会限制吞吐量。

采样通过删除重复的日志条目来解决这个问题。在正常情况下,您的应用程序会输出每个记录。但是,当类似的记录每秒输出数百或数千次时,zap 开始丢弃重复以保存吞吐量。

为什么结构化的日志 API 除了接受字段之外还可以接收消息?

主观上,我们发现在结构化上下文中附带一个简短的描述是有帮助的。这在开发过程中并不关键,但它使调试和操作不熟悉的系统更加容易。

更具体地说,zap 的采样算法使用消息来识别重复的条目。根据我们的经验,这是一个介于随机抽样(通常在调试时删除您需要的确切条目)和哈希完整条目(代价高)之间的一个中间方法。

为什么要包括全局 loggers?

由于许多其他日志包都包含全局变量logger,许多应用程序没有设计成接收logger作为显式参数。更改函数签名通常是一种破坏性的更改,因此zap包含全局logger以简化迁移。

尽可能避免使用它们。

为什么包括专用的Panic和Fatal日志级别?

一般来说,应用程序代码应优雅地处理错误,而不是使用panicos.Exit。但是,每个规则都有例外,当错误确实无法恢复时,崩溃是很常见的。为了避免丢失任何信息(尤其是崩溃的原因),记录器必须在进程退出之前冲洗任何缓冲条目。

Zap 通过提供在退出前自动冲洗的PanicFatal记录方法来使这一操作变得简单。当然,这并不保证日志永远不会丢失,但它消除了常见的错误。

有关详细信息,请参阅 Uber-go/zap#207 中的讨论。

什么是DPanic?

DPanic代表"panic in development."。在development中,它会打印Panic级别的日志:反之,它将发生在Error级别的日志,DPanic更加容易捕获可能但实际上不应该发生的错误,而不是在生产环境中Panic。

如果你曾经写过这样的代码,就可以使用DPanic:

  1.   if err != nil {
  2.     panic(fmt.Sprintf("shouldn't ever get here: %v", err))
  3.   }

安装问题

错误expects import "go.uber.org/zap"是什么意思?

要么zap安装错误,要么您引用了代码中的错误包名。

Zap 的源代码托管在 GitHub 上,但  import path[3]是  go.uber.org/zap,让我们项目维护者,可以更方便地自由移动源代码。所以在安装和使用包时需要注意这一点。

如果你遵循两个简单的规则,就会正常工作:安装zapgo get -u go.uber.org/zap并始终导入它在你的代码import "go.uber.org/zap",代码不应包含任何对github.com/uber-go/zap的引用.

用法问题

Zap是否支持日志切割?

Zap 不支持切割日志文件,因为我们更喜欢将此交给外部程序,如logrotate.

但是,日志切割包很容易集成,如  `gopkg.in/natefinch/lumberjack.v2`[4] 作为zapcore.WriteSyncer.

  1.   // lumberjack.Logger is already safe for concurrent use, so we don't need to
  2.   // lock it.
  3.   w := zapcore.AddSync(&lumberjack.Logger{
  4.     Filename:   "/var/log/myapp/foo.log",
  5.     MaxSize:    500, // megabytes
  6.     MaxBackups: 3,
  7.     MaxAge:     28, // days
  8.   })
  9.   core := zapcore.NewCore(
  10.     zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
  11.     w,
  12.     zap.InfoLevel,
  13.   )
  14.   logger := zap.New(core)

插件

我们很希望zap 本身能满足的每一个logging需求,但我们只熟悉少数日志摄入(log ingestion)系统、参数解析(flag-parsing)包等。所以我们更愿意发展 zap 插件生态系统。

下面扩展包,可以作为参考使用:

 

集成
github.com/tchap/zapext Sentry, syslog
github.com/fgrosse/zaptest Ginkgo
github.com/blendle/zapdriver Stackdriver
github.com/moul/zapgorm Gorm

 

性能比较

说明 : 以下资料来源于 zap 官方,Zap 提供的基准测试清楚地表明,zerolog[5]是与 Zap 竞争最激烈的。zerolo还提供结果非常相似的基准测试[6]

记录一个10个kv字段的消息:

 

PackageTimeTime % to zapObjects Allocated
⚡ zap 862 ns/op +0% 5 allocs/op
⚡ zap (sugared) 1250 ns/op +45% 11 allocs/op
zerolog 4021 ns/op +366% 76 allocs/op
go-kit 4542 ns/op +427% 105 allocs/op
apex/log 26785 ns/op +3007% 115 allocs/op
logrus 29501 ns/op +3322% 125 allocs/op
log15 29906 ns/op +3369% 122 allocs/op

 

使用一个已经有10个kv字段的logger记录一条消息:

 

PackageTimeTime % to zapObjects Allocated
⚡ zap 126 ns/op +0% 0 allocs/op
⚡ zap (sugared) 187 ns/op +48% 2 allocs/op
zerolog 88 ns/op -30% 0 allocs/op
go-kit 5087 ns/op +3937% 103 allocs/op
log15 18548 ns/op +14621% 73 allocs/op
apex/log 26012 ns/op +20544% 104 allocs/op
logrus 27236 ns/op +21516% 113 allocs/op

 

记录一个字符串,没有字段或printf风格的模板:

 

PackageTimeTime % to zapObjects Allocated
⚡ zap 118 ns/op +0% 0 allocs/op
⚡ zap (sugared) 191 ns/op +62% 2 allocs/op
zerolog 93 ns/op -21% 0 allocs/op
go-kit 280 ns/op +137% 11 allocs/op
standard library 499 ns/op +323% 2 allocs/op
apex/log 1990 ns/op +1586% 10 allocs/op
logrus 3129 ns/op +2552% 24 allocs/op
log15 3887 ns/op +3194% 23 allocs/op

 

相似的库

logrus[7] 功能强大

zerolog[8] 性能相当好的日志库

参考资料

[1] 

⚡ZAP: https://github.com/uber-go/zap

[2] 

Rob Pike 谚语指出: https://go-proverbs.github.io/

[3] 

import path: https://golang.org/cmd/go/#hdr-Remote_import_paths

[4] 

gopkg.in/natefinch/lumberjack.v2: https://godoc.org/gopkg.in/natefinch/lumberjack.v2

[5] 

zerolog: https://github.com/rs/zerolog

[6] 

基准测试: https://github.com/rs/zerolog#benchmarks

[7] 

logrus: https://github.com/sirupsen/logrus

[8] 

zerolog: https://github.com/rs/zerolog

标签:由浅入深,zapcore,ce,Zap,Go,日志,logger,zap,op
From: https://www.cnblogs.com/liujiacai/p/17341075.html

相关文章

  • Invalid prop: type check failed for prop "defaultExpandAll". Expected Boolean, g
    vue中使用element-ui报错如下,defaultExpandAll关键词页面也搜不到[Vuewarn]:Invalidprop:typecheckfailedforprop"defaultExpandAll".ExpectedBoolean,gotStringwithvalue"true".foundin---><ElTable>atpackages/table/src/table.vue......
  • go类型
    1、基本类型强转//interface{}转为其他类型【当然这个得保证是这个类型,否则肯定报错。最好先断言】varvinterface{}varainta=v.(int)//string转为int类型//uint32转为int【低精度往高精度转?】variuint32a=int(i)参考:https://blog.csdn.net/iamlihon......
  • go mod
    gomodtidy可能会修改指定的依赖版本号gomod的最小版本号选择的其实是选择所有package指定的mod的最大版本号你指定了v2.1.0,但是依赖的某一个包指定了v2.2.1,最终编译就使用v2.2.1来编译......
  • (转)跟我一起学Go系列:日志系统从入门到晋级
    原文:https://zhuanlan.zhihu.com/p/361930459日志模块在如今的应用中地位是如日中天,开发者没有日志就相当于双目失明,对程序的运行状态无法判断。Go也不例外提供了基础的日志调用模块:log模块。log模块主要提供了3类接口,分别是“Print、Panic、Fatal”,下面一起看看基础日......
  • golang中通过原始socket实现tcp/udp的服务端和客户端示例
    这些天稍微空点,总结下golang中通过tcp/udp实现服务端客户端的编程实现,毕竟长久以来,如果要截单的http服务,我们直接使用net/http包实现服务,或者使用框架如gin/echo/beego等。以下就直接上代码,稍微看看都能懂起。1.TCP的实现serverpackagemainimport( "bufio" "fmt" "net"......
  • golang 中常用的超时控制的方案示例
    在go中,我们很容易就可以实现超时控制,今天分享2种解决方案:1.select+time.After2.select+context其实两种方案中,我们都是通过channel来控制的,在方案1中,对于time.After,通过返回一个只读<-chanTime实现,而context中,则通过context.Done()实现,通过返回<-chans......
  • 通过django-background-tasks执行定时任务
    1.安装django-background-taskspipinstalldjango-background-tasks2.在Django项目的settings.py文件中添加以app:INSTALLED_APPS=[#otherapps'background_task',]3.创建一个包含需要执行的任务函数:frombackground_taskimportbackgroundimportrando......
  • Google Earth Engine(GEE)——全球河流网络及相应的水资源区数据集
    全球河流网络及相应的水资源区河流网络和水资源区(WRZ)对于水资源的规划、利用、开发、保护和管理至关重要。目前,世界上的河网和水资源区大多是根据数字高程模型数据自动获得的,这些数据不够准确,尤其是在平原地区。此外,WRZ代码与河网不一致。作者提出了一系列方法,生成了分辨率较高、......
  • django使用多个数据库实现
    一、说明:在开发Django项目的时候,很多时候都是使用一个数据库,即settings中只有default数据库,但是有一些项目确实也需要使用多个数据库,这样的项目,在数据库配置和使用的时候,就比较麻烦一点。二、Django使用多个数据库中settings中的DATABASES的设置2.1默认只是用一......
  • MongoDriver 分表分页查询
    摘要:业务需求,分表也要兼容旧表。技术有限,封装思路及代码如下,大佬们见笑。首先Mongdb的Collection及其内容字段都是可以动态创建的,所以这里需要的一个关键点是,分表时用什么字段。本文将使用数据的创建时间作为依据,按月分表(如果需要其它字段分表,也可以参考这个思路)首先本文使......