Skip to content

Zap

Zap 是一個用 Go 構建的,快速的 ,結構化,級別化的日志組件。

官方倉庫:uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)

官方文檔:zap package - go.uber.org/zap - Go Packages

安裝

go get -u go.uber.org/zap

快速開始

官方給出了兩個快速開始的示例,兩個都是產品級別的日志,第一個是一個支持printf風格但是性能相對較低的Sugar

go
logger, _ := zap.NewProduction()
defer logger.Sync() // 在程序結束時將緩存同步到文件中
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

第二個是性能比較好,但是僅支持強類型輸出的日志·logger

go
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
  // Structured context as strongly typed Field values.
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

TIP

Zap 的使用非常簡單,麻煩的點在於配置出一個適合自己項目的日志,官方例子很少,要多讀源代碼注釋。

配置

一般來說日志的配置都是寫在配置文件裡的,Zap 的配置也支持通過配置文件反序列化,但是僅支持基礎的配置,即便是高級配置官方給出的例子也是十分簡潔,並不足以投入使用,所以要詳細講一下細節的配置。

首先看一下總體的配置結構體,需要先搞明白裡面的每一個字段的含義

go
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"`
    // 給日志添加一些默認輸出的內容
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

如下是關於編碼配置的細節

go
type EncoderConfig struct {
   // 鍵值,如果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"`
   SkipLineEnding bool   `json:"skipLineEnding" yaml:"skipLineEnding"`
   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"`
   // 反射編碼器,主要是對於interface{}類型,如果沒有默認jsonencoder
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // 控制台輸出間隔字符串
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option是關於一些配置的開關及應用,有很多實現。

go
type Option interface {
   apply(*Logger)
}

// Option的實現
type optionFunc func(*Logger)

func (f optionFunc) apply(log *Logger) {
  f(log)
}

// 應用
func Development() Option {
  return optionFunc(func(log *Logger) {
    log.development = true
  })
}

這是最常用的日志核心,其內部的字段基本上就代表了我們配置的步驟,也可以參考官方在反序列化配置時的步驟,大致都是一樣的。

go
type ioCore struct {
   // 日志級別
   LevelEnabler
   // 日志編碼
   enc Encoder
   // 日志書寫
   out WriteSyncer
}

zap.Encoder 負責日志的格式化,編碼

zap.WriteSyncer 負責日志的輸出,主要是輸出到文件和控制台

zap.LevelEnabler 最小日志級別,該級別以下的日志不會再通過syncer輸出。

日志編碼

日志編碼主要涉及到對於日志的一些細節的格式化,首先看一下直接使用最原始的日志的輸出。

go
func TestQuickStart(t *testing.T) {
   rawJSON := []byte(`{
     "level": "debug",
     "encoding": "json",
     "outputPaths": ["stdout"],
     "errorOutputPaths": ["stderr"],
     "initialFields": {"foo": "bar"},
     "encoderConfig": {
       "messageKey": "message",
       "levelKey": "level",
       "levelEncoder": "lowercase"
     }
   }`)

   var cfg zap.Config
   if err := json.Unmarshal(rawJSON, &cfg); err != nil {
      panic(err)
   }
   logger := zap.Must(cfg.Build())
   defer logger.Sync()

   logger.Info("logger construction succeeded")
}
{"level":"info","message":"logger construction succeeded","foo":"bar"}

會發現這行日志有幾個問題:

  • 沒有時間
  • 沒有調用者的情況,不知道這行日志是哪裡輸出的,不然到時候發生錯誤的話都沒法排查
  • 沒有堆棧情況

接下來就一步一步的來解決問題,主要是對zapcore.EncoderConfig來進行改造,首先我們要自己書寫配置文件,不采用官方的直接反序列化。首先自己創建一個配置文件config.yml

yml
# Zap日志配置
zap:
  prefix: ZapLogTest
  timeFormat: 2006/01/02 - 15:04:05.00000
  level: debug
  caller: true
  stackTrace: false
  encode: console
  # 日志輸出到哪裡 file | console | both
  writer: both
  logFile:
    maxSize: 20
    backups: 5
    compress: true
    output:
      - "./log/output.log"

映射到的結構體

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: zap日志配置結構體
type ZapConfig struct {
  Prefix     string         `yaml:"prefix" mapstructure:""prefix`
  TimeFormat string         `yaml:"timeFormat" mapstructure:"timeFormat"`
  Level      string         `yaml:"level" mapstructure:"level"`
  Caller     bool           `yaml:"caller" mapstructure:"caller"`
  StackTrace bool           `yaml:"stackTrace" mapstructure:"stackTrace"`
  Writer     string         `yaml:"writer" mapstructure:"writer"`
  Encode     string         `yaml:"encode" mapstructure:"encode"`
  LogFile    *LogFileConfig `yaml:"logFile" mapstructure:"logFile"`
}

// LogFileConfig
// @Date: 2023-01-09 16:38:45
// @Description: 日志文件配置結構體
type LogFileConfig struct {
  MaxSize  int      `yaml:"maxSize" mapstructure:"maxSize"`
  BackUps  int      `yaml:"backups" mapstructure:"backups"`
  Compress bool     `yaml:"compress" mapstructure:"compress"`
  Output   []string `yaml:"output" mapstructure:"output"`
  Errput   []string `yaml:"errput" mapstructure:"errput"`
}

TIP

讀取配置使用Viper,具體代碼省略。

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimerEncoder本質上其實是一個函數,我們可以采用官方提供的其他時間編碼器,也可以自行編寫。

go
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
   encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}

整體部分如下

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // 新建一個配置
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // 自定義時間格式
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // 日志級別大寫
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // 秒級時間間隔
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // 簡短的調用者輸出
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // 完整的序列化logger名稱
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // 最終的日志編碼 json或者console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // 默認console
   return zapcore.NewConsoleEncoder(encoderConfig)
}

日式輸出

日志輸出分為控制台輸出和文件輸出,我們可以根據配置文件來進行動態配置,並且如果想要進行日志文件切割的話還需要使用另一個第三方的依賴。

go get -u github.com/natefinch/lumberjack

最後代碼如下

go
 func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // 如果開啟了日志控制台輸出,就加入控制台書寫器
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // 如果開啟了日志文件存儲,就根據文件路徑切片加入書寫器
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // 添加日志輸出器
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, //文件路徑
            MaxSize:    cfg.LogFile.MaxSize, //分割文件的大小
            MaxBackups: cfg.LogFile.BackUps, //備份次數
            Compress:   cfg.LogFile.Compress, // 是否壓縮
            LocalTime:  true, //使用本地時間
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

日志級別

官方有關於日志級別的枚舉項,直接使用即可。

go
func zapLevelEnabler(cfg *ZapConfig) zapcore.LevelEnabler {
   switch cfg.Level {
   case config.DebugLevel:
      return zap.DebugLevel
   case config.InfoLevel:
      return zap.InfoLevel
   case config.ErrorLevel:
      return zap.ErrorLevel
   case config.PanicLevel:
      return zap.PanicLevel
   case config.FatalLevel:
      return zap.FatalLevel
   }
   // 默認Debug級別
   return zap.DebugLevel
}

最後構建

go
func InitZap(config *ZapConfig) *zap.Logger {
   // 構建編碼器
   encoder := zapEncoder(config)
   // 構建日志級別
   levelEnabler := zapLevelEnabler(config)
   // 最後獲得Core和Options
   subCore, options := tee(config, encoder, levelEnabler)
    // 創建Logger
   return zap.New(subCore, options...)
}

// 將所有合並
func tee(cfg *ZapConfig, encoder zapcore.Encoder, levelEnabler zapcore.LevelEnabler) (core zapcore.Core, options []zap.Option) {
   sink := zapWriteSyncer(cfg)
   return zapcore.NewCore(encoder, sink, levelEnabler), buildOptions(cfg, levelEnabler)
}

// 構建Option
func buildOptions(cfg *ZapConfig, levelEnabler zapcore.LevelEnabler) (options []zap.Option) {
   if cfg.Caller {
      options = append(options, zap.AddCaller())
   }

   if cfg.StackTrace {
      options = append(options, zap.AddStacktrace(levelEnabler))
   }
   return
}

最後效果

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     日志初始化完成

Golang學習網由www.golangdev.cn整理維護