一文帶你由淺入深地解讀 Zap 的高性能

簡介

zap 是什麼?

⚡ZAP[1] 是 uber 開源的提供快速,結構化,高性能的日誌記錄包。

zap 高性能體現在哪裏?

在介紹 zap 包的優化部分之前,讓我們看下 zap 日誌庫的工作流程圖

大多數日誌庫提供的方式是基於反射的序列化和字符串格式化,這種方式代價高昂,而 Zap 採取不同的方法。

logger.Info("failed to fetch URL",
  zap.String("url", "https://baidu.com"),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

詳情在下文 zapcore 模塊介紹。

Example

安裝

go get -u go.uber.org/zap

Zap 提供了兩種類型的 logger

性能良好但不是關鍵的情況下,使用 SugaredLogger,它比其他結構化的日誌包快 4-10 倍,並且支持結構化和 printf 風格的 APIs。

例一 調用 NewProduction 創建 logger 對象

func TestSugar(t *testing.T) {
 logger, _ := zap.NewProduction()
 // 默認 logger 不緩衝。
 // 但由於底層 api 允許緩衝,所以在進程退出之前調用 Sync 是一個好習慣。
 defer logger.Sync()
 sugar := logger.Sugar()
 sugar.Infof("Failed to fetch URL: %s", "https://baidu.com")
}

性能和類型安全要求嚴格的情況下,可以使用 Logger ,它甚至比前者 SugaredLogger 更快,內存分配次數也更少,但它僅支持強類型的結構化日誌記錄。

例二 調用 NewDevelopment 創建 logger 對象

func TestLogger(t *testing.T) {
 logger, _ := zap.NewDevelopment()
 defer logger.Sync()
 logger.Info("failed to fetch URL",
  // 強類型字段
  zap.String("url", "https://baidu.com"),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
 )
}

不需要爲整個應用程序決定選擇使用 Logger 還是 SugaredLogger ,兩者之間都可以輕鬆轉換。

例三 Logger 與 SugaredLogger 相互轉換

// 創建 logger
logger := zap.NewExample()
defer logger.Sync()

// 轉換 SugaredLogger
sugar := logger.Sugar()
// 轉換 logger
plain := sugar.Desugar()

例四 自定義格式

自定義一個日誌消息格式,帶着問題看下列代碼。

  1. debug 級別的日誌打印到控制檯了嗎?

  2. 最後的 error 會打印到控制檯嗎 ?

package main

import (
 "os"

 "go.uber.org/zap"
 "go.uber.org/zap/zapcore"
)

func NewCustomEncoderConfig() zapcore.EncoderConfig {
 return zapcore.EncoderConfig{
  TimeKey:        "ts",
  LevelKey:       "level",
  NameKey:        "logger",
  CallerKey:      "caller",
  FunctionKey:    zapcore.OmitKey,
  MessageKey:     "msg",
  StacktraceKey:  "stacktrace",
  LineEnding:     zapcore.DefaultLineEnding,
  EncodeLevel:    zapcore.CapitalColorLevelEncoder,
  EncodeTime:     zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05"),
  EncodeDuration: zapcore.SecondsDurationEncoder,
  EncodeCaller:   zapcore.ShortCallerEncoder,
 }
}

func main() {
 atom := zap.NewAtomicLevelAt(zap.DebugLevel)
 core := zapcore.NewCore(
  zapcore.NewConsoleEncoder(NewCustomEncoderConfig()),
  zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
  atom,
 )
 logger := zap.New(core, zap.AddCaller(), zap.Development())
 defer logger.Sync()

 // 配置 zap 包的全局變量
 zap.ReplaceGlobals(logger)

 // 運行時安全地更改 logger 日記級別
 atom.SetLevel(zap.InfoLevel)
 sugar := logger.Sugar()
 // 問題 1: debug 級別的日誌打印到控制檯了嗎?
 sugar.Debug("debug")
 sugar.Info("info")
 sugar.Warn("warn")
 sugar.DPanic("dPanic")
 // 問題 2: 最後的 error 會打印到控制檯嗎?
 sugar.Error("error")
}

結果見下圖

image-20210525201656456

問題 1:

沒有打印。AtomicLevel 是原子性可更改的動態日誌級別,通過調用 atom.SetLevel 更改日誌級別爲 infoLevel 。

問題 2:

沒有打印。zap.Development() 啓用了開發模式,在開發模式下 DPanic 函數會引發 panic,所以最後的 error 不會打印到控制檯。

源碼分析

此次源碼分析基於 Zap 1.16

zap 概覽

上圖僅表示 zap 可調用兩種 logger,沒有表達 Logger 與 SugaredLogger 的關係,繼續往下看,你會更理解。

Logger

logger 提供快速,分級,結構化的日誌記錄。所有的方法都是安全的,內存分配很重要,因此它的 API 有意偏向於性能和類型安全。

zap@v1.16.0 - logger.go

type Logger struct {
  // 實現編碼和輸出的接口
 core zapcore.Core  
  // 記錄器開發模式,DPanic 等級將記錄 panic
 development bool
  // 開啓記錄調用者的行號和函數名
 addCaller   bool  
  // 致命日誌採取的操作,默認寫入日誌後 os.Exit()
  onFatal     zapcore.CheckWriteAction 
 name        string 
  // 設置記錄器生成的錯誤目的地
 errorOutput zapcore.WriteSyncer  
  // 記錄 >= 該日誌等級的堆棧追蹤
 addStack zapcore.LevelEnabler 
  // 避免記錄器認爲封裝函數爲調用方
 callerSkip int 
  // 默認爲系統時間 
 clock Clock  
}

在 Example 中分別使用了 NewProduction 和 NewDevelopment ,接下來以這兩個函數開始分析。下圖表示 A 函數調用了 B 函數,其中箭頭表示函數調用關係。圖中函數都會分析到。

NewProduction

從下面代碼中可以看出,此函數是對 NewProductionConfig().Build(...) 封裝的快捷方式。

zap@v1.16.0 - logger.go

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

NewProductionConfig

在 InfoLevel 及更高級別上啓用了日誌記錄。它使用 JSON 編碼器,寫入 stderr,啓用採樣。

zap@v1.16.0 - config.go

func NewProductionConfig() Config {
 return Config{
    // info 日誌級別
  Level:       NewAtomicLevelAt(InfoLevel),
    // 非開發模式
  Development: false,
    // 採樣設置
  Sampling: &SamplingConfig{
   Initial:    100, // 相同日誌級別下相同內容每秒日誌輸出數量
   Thereafter: 100, // 超過該數量,纔會再次輸出
  },
    // JSON 編碼器
  Encoding:         "json",
    // 後面介紹
  EncoderConfig:    NewProductionEncoderConfig(),
    // 輸出到 stderr
  OutputPaths:      []string{"stderr"},
  ErrorOutputPaths: []string{"stderr"},
 }
}

Config 結構體

通過 Config 可以設置通用的配置項。

zap@v1.16.0 - config.go

type Config struct {
 // 日誌級別
 Level AtomicLevel `json:"level" yaml:"level"`
 // 開發模式
 Development bool `json:"development" yaml:"development"`
 // 停止使用調用方的函數和行號
 DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
 // 完全停止使用堆棧跟蹤,默認爲  `>=WarnLevel` 使用堆棧跟蹤
 DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
 // 採樣設置策略
 Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
 // 記錄器的編碼,有效值爲 'json' 和 'console' 以及通過 `RegisterEncoder` 註冊的有效編碼
 Encoding string `json:"encoding" yaml:"encoding"`
 // 編碼器選項
 EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
 // 日誌的輸出路徑
 OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
 // zap 內部錯誤的輸出路徑
 ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
 // 添加到根記錄器的字段的集合
 InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

NewDevelopment

從下面代碼中可以看出,此函數是對 NewDevelopmentConfig().Build(...) 封裝的快捷方式

zap@v1.16.0 - logger.go

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

NewDevelopmentConfig

此函數在 DebugLevel 及更高版本上啓用日誌記錄,它使用 console 編碼器,寫入 stderr,禁用採樣。

zap@v1.16.0 - config.go

func NewDevelopmentConfig() Config {
 return Config{
    // debug 等級
  Level:            NewAtomicLevelAt(DebugLevel),
    // 開發模式
  Development:      true,
    // console 編碼器
  Encoding:         "console",
  EncoderConfig:    NewDevelopmentEncoderConfig(),
    // 輸出到 stderr
  OutputPaths:      []string{"stderr"},
  ErrorOutputPaths: []string{"stderr"},
 }
}

NewProductionEncoderConfig 和 NewDevelopmentEncoderConfig 都是返回編碼器配置。

zap@v1.16.0 - config.go

type EncoderConfig struct {
 // 設置 編碼爲 JSON 時的 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"`
 FunctionKey   string `json:"functionKey" yaml:"functionKey"`
 StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
  // 配置行分隔符
 LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
 // 配置常見覆雜類型的基本表示形式。
 EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
 EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
 EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
 EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
 // 日誌名稱,此參數可選
 EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
 // 配置 console 編碼器使用的字段分隔符,默認 tab
 ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

NewProductionEncoderConfig

zap@v1.16.0 - config.go

func NewProductionEncoderConfig() zapcore.EncoderConfig {
 return zapcore.EncoderConfig{
  TimeKey:        "ts",
  LevelKey:       "level",
  NameKey:        "logger",
  CallerKey:      "caller",
  FunctionKey:    zapcore.OmitKey,
  MessageKey:     "msg",
  StacktraceKey:  "stacktrace",
    // 默認換行符 \n
  LineEnding:     zapcore.DefaultLineEnding,
    // 日誌等級序列爲小寫字符串,如:InfoLevel被序列化爲 "info"
  EncodeLevel:    zapcore.LowercaseLevelEncoder,
    // 時間序列化成浮點秒數
  EncodeTime:     zapcore.EpochTimeEncoder,
    // 時間序列化,Duration爲經過的浮點秒數
  EncodeDuration: zapcore.SecondsDurationEncoder,
    // 以 包名/文件名:行數 格式序列化
  EncodeCaller:   zapcore.ShortCallerEncoder,
 }
}

該配置會輸出如下結果,此結果出處參見 Example 中的例一

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

NewDevelopmentEncoderConfig

zap@v1.16.0 - config.go

func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
 return zapcore.EncoderConfig{
  // keys 值可以是任意非空的值
  TimeKey:        "T",
  LevelKey:       "L",
  NameKey:        "N",
  CallerKey:      "C",
  FunctionKey:    zapcore.OmitKey,
  MessageKey:     "M",
  StacktraceKey:  "S",
     // 默認換行符 \n
  LineEnding:     zapcore.DefaultLineEnding,
    // 日誌等級序列爲大寫字符串,如:InfoLevel被序列化爲 "INFO"
  EncodeLevel:    zapcore.CapitalLevelEncoder,
    // 時間格式化爲  ISO8601 格式
  EncodeTime:     zapcore.ISO8601TimeEncoder,
  EncodeDuration: zapcore.StringDurationEncoder,
    // // 以 包名/文件名:行數 格式序列化
  EncodeCaller:   zapcore.ShortCallerEncoder,
 }
}

該配置會輸出如下結果,此結果出處參見 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,接下來我們看看這個函數。

zap@v1.16.0 - config.go

func (cfg Config) Build(opts ...Option) (*Logger, error) {
  enc, err := cfg.buildEncoder()
 if err != nil {
  return nil, err
 }

 sink, errSink, err := cfg.openSinks()
 if err != nil {
  return nil, err
 }
 
 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
}

從上面的代碼中,通過解析 config 的參數,調用 New 方法來創建 Logger。在 Example 中例四,就是調用 New 方法來自定義 Logger。

SugaredLogger

Logger 作爲 SugaredLogger 的屬性,這個封裝優點在於不是很在乎性能的情況下,可以快速調用Logger。所以名字爲加了糖的 Logger。

zap@v1.16.0 - logger.go

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

Logger不同,SugaredLogger不強制日誌結構化。所以對於每個日誌級別,都提供了三種方法。

level

zap@v1.16.0 - sugar.go

以 info 級別爲例,相關的三種方法。

// Info 使用 fmt.Sprint 構造和記錄消息。
func (s *SugaredLogger) Info(args ...interface{}) {
 s.log(InfoLevel, "", args, nil)
}

// Infof 使用 fmt.Sprintf 記錄模板消息。
func (s *SugaredLogger) Infof(template string, args ...interface{}) {
 s.log(InfoLevel, template, args, nil)
}

// Infow 記錄帶有其他上下文的消息
func (s *SugaredLogger) Infow(msg string, keysAndValues ...interface{}) {
 s.log(InfoLevel, msg, nil, keysAndValues)
}

在 sugar.Infof("...") 打上斷點,從這開始追蹤源碼。

image-20210519111252185

在調試代碼之前,先給大家看一下SugaredLogger 的  Infof 函數的調用的大致工作流,其中不涉及採樣等。

infof 工作流程

Info , InfofInfow 三個函數都調用了 log 函數,log 函數代碼如下

zap@v1.16.0 - sugar.go

func (s *SugaredLogger) log(lvl zapcore.Level, template string, fmtArgs []interface{}, context []interface{}) {
 // 判斷是否啓用的日誌級別
 if lvl < DPanicLevel && !s.base.Core().Enabled(lvl) {
  return
 }
 // 將參數合併到語句中
 msg := getMessage(template, fmtArgs)
  // Check 可以幫助避免分配一個分片來保存字段。
 if ce := s.base.Check(lvl, msg); ce != nil {
  ce.Write(s.sweetenFields(context)...)
 }
}

函數的第一個參數 InfoLevel 是日誌級別,其源碼如下

zap@v1.16.0 - zapcore/level.go

const (
 // Debug 應是大量的,且通常在生產狀態禁用.
 DebugLevel = zapcore.DebugLevel
 // Info 是默認的記錄優先級.
 InfoLevel = zapcore.InfoLevel
 // Warn 比 info 更重要.
 WarnLevel = zapcore.WarnLevel
 // Error 是高優先級的,如果程序順利不應該產生任何 err 級別日誌.
 ErrorLevel = zapcore.ErrorLevel
 // DPanic 特別重大的錯誤,在開發模式下引起 panic. 
 DPanicLevel = zapcore.DPanicLevel
 // Panic 記錄消息後調用 panic.
 PanicLevel = zapcore.PanicLevel
 // Fatal 記錄消息後調用 os.Exit(1).
 FatalLevel = zapcore.FatalLevel
)

getMessage 函數處理 template 和 fmtArgs 參數,主要爲不同的參數選擇最合適的方式拼接消息

zap@v1.16.0 - sugar.go

func getMessage(template string, fmtArgs []interface{}) string {
  // 沒有參數直接返回 template
 if len(fmtArgs) == 0 {
  return template
 }
 
  // 此處調用 Sprintf 會使用反射
 if template != "" {
  return fmt.Sprintf(template, fmtArgs...)
 }
 
  // 消息爲空並且有一個參數,返回該參數
 if len(fmtArgs) == 1 {
  if str, ok := fmtArgs[0].(string); ok {
   return str
  }
 }
  // 返回所有 fmtArgs
 return fmt.Sprint(fmtArgs...)
}

關於 s.base.Check ,這就需要介紹zapcore ,下面分析相關模塊。

zapcore

zapcore包 定義並實現了構建 zap 的低級接口。通過提供這些接口的替代實現,外部包可以擴展 zap 的功能。

zap@v1.16.0 - zapcore/core.go

// Core 是一個最小的、快速的記錄器接口。
type Core interface {
  // 接口,決定一個日誌等級是否啓用
 LevelEnabler
 // 向 core 添加核心上下文
 With([]Field) Core
 // 檢查是否應記錄提供的條目
  // 在調用 write 之前必須先調用 Check
 Check(Entry, *CheckedEntry) *CheckedEntry
 // 寫入日誌
 Write(Entry, []Field) error
  // 同步刷新緩存日誌(如果有)
 Sync() error
}

Check 函數有兩個入參。第一個參數表示一條完整的日誌消息,第二個參數爲 nil 時會從 sync.Pool 創建的池中取出*CheckedEntry對象複用,避免重新分配內存。該函數內部調用 AddCore 實現獲取 *CheckedEntry對象,最後調用 Write 寫入日誌消息。

相關代碼全部貼在下面,更多介紹請看代碼中的註釋。

zap@v1.16.0 - zapcore/entry.go

// 一個 entry 表示一個完整的日誌消息
type Entry struct {
 Level      Level
 Time       time.Time
 LoggerName string
 Message    string
 Caller     EntryCaller
 Stack      string
}
// 使用 sync.Pool 複用臨時對象
var (
 _cePool = sync.Pool{New: func() interface{} {
  return &CheckedEntry{
   cores: make([]Core, 4),
  }
 }}
)

// 從池中取出 CheckedEntry 並初始化值
func getCheckedEntry() *CheckedEntry {
 ce := _cePool.Get().(*CheckedEntry)
 ce.reset()
 return ce
}


// CheckedEntry 是 enter 和 cores 集合。
type CheckedEntry struct {
 Entry
 ErrorOutput WriteSyncer
 dirty       bool  // 用於檢測是否重複使用對象
 should      CheckWriteAction // 結束程序的動作
 cores       []Core
}

// 重置對象
func (ce *CheckedEntry) reset() {
 ce.Entry = Entry{}
 ce.ErrorOutput = nil
 ce.dirty = false
 ce.should = WriteThenNoop
 for i := range ce.cores {
  // 不要保留對 core 的引用!!
  ce.cores[i] = nil
 }
 ce.cores = ce.cores[:0]
}

// 將 entry 寫入存儲的 cores
// 最後將 CheckedEntry 添加到池中
func (ce *CheckedEntry) Write(fields ...Field) {
 if ce == nil {
  return
 }

 if ce.dirty {
  if ce.ErrorOutput != nil {
      // 檢查 CheckedEntry 的不安全重複使用
   fmt.Fprintf(ce.ErrorOutput, "%v Unsafe CheckedEntry re-use near Entry %+v.\n", ce.Time, ce.Entry)
   ce.ErrorOutput.Sync()
  }
  return
 }
 ce.dirty = true

 var err error
  // 寫入日誌消息
 for i := range ce.cores {
  err = multierr.Append(err, ce.cores[i].Write(ce.Entry, fields))
 }
  // 處理內部發生的錯誤
 if ce.ErrorOutput != nil {
  if err != nil {
   fmt.Fprintf(ce.ErrorOutput, "%v write error: %v\n", ce.Time, err)
   ce.ErrorOutput.Sync()
  }
 }

 should, msg := ce.should, ce.Message
  // 將 CheckedEntry 添加到池中,下次複用
 putCheckedEntry(ce)
 
  // 判斷是否需要 panic 或其它方式終止程序..
 switch should {
 case WriteThenPanic:
  panic(msg)
 case WriteThenFatal:
  exit.Exit()
 case WriteThenGoexit:
  runtime.Goexit()
 }
}

func (ce *CheckedEntry) AddCore(ent Entry, core Core) *CheckedEntry {
 if ce == nil {
    // 從池中取 CheckedEntry,減少內存分配
  ce = getCheckedEntry()
  ce.Entry = ent
 }
 ce.cores = append(ce.cores, core)
 return ce
}

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:

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

安裝問題

錯誤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.

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

插件

我們很希望 zap 本身能滿足的每一個 logging 需求,但我們只熟悉少數日誌攝入 (log ingestion) 系統、參數解析 (flag-parsing) 包等。所以我們更願意發展 zap 插件生態系統。

下面擴展包,可以作爲參考使用:

zjG8kD

性能比較

說明 : 以下資料來源於 zap 官方,Zap 提供的基準測試清楚地表明,zerolog[5] 是與 Zap 競爭最激烈的。zerolo 還提供結果非常相似的基準測試 [6]:

記錄一個 10 個 kv 字段的消息:

xvt3xe

使用一個已經有 10 個 kv 字段的 logger 記錄一條消息:

SAZvHC

記錄一個字符串,沒有字段或printf風格的模板:

spl77I

相似的庫

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Qc5tGELKPZOA9LEBOEkuow