Go 每日一庫之 zap

簡介

在很早以前的文章中,咱們介紹過 Go 標準日誌庫log和結構化的日誌庫logrus。在熱點函數中記錄日誌對日誌庫的執行性能有較高的要求,不能影響正常邏輯的執行時間。uber開源的日誌庫zap,對性能和內存分配作了極致的優化。git

快速使用

先安裝:github

$ go get go.uber.org/zap

後使用:golang

package main

import (
  "time"

  "go.uber.org/zap"
)

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  url := "http://example.org/api"
  logger.Info("failed to fetch URL",
    zap.String("url", url),
    zap.Int("attempt", 3),
    zap.Duration("backoff", time.Second),
  )

  sugar := logger.Sugar()
  sugar.Infow("failed to fetch URL",
    "url", url,
    "attempt", 3,
    "backoff", time.Second,
  )
  sugar.Infof("Failed to fetch URL: %s", url)
}

zap庫的使用與其餘的日誌庫很是類似。先建立一個logger,而後調用各個級別的方法記錄日誌(Debug/Info/Error/Warn)。zap提供了幾個快速建立logger的方法,zap.NewExample()zap.NewDevelopment()zap.NewProduction(),還有高度定製化的建立方法zap.New()。建立前 3 個logger時,zap會使用一些預約義的設置,它們的使用場景也有所不一樣。Example適合用在測試代碼中,Development在開發環境中使用,Production用在生成環境。web

zap底層 API 能夠設置緩存,因此通常使用defer logger.Sync()將緩存同步到文件中。json

因爲fmt.Printf之類的方法大量使用interface{}和反射,會有很多性能損失,而且增長了內存分配的頻次。zap爲了提升性能、減小內存分配次數,沒有使用反射,並且默認的Logger只支持強類型的、結構化的日誌。必須使用zap提供的方法記錄字段。zap爲 Go 語言中全部的基本類型和其餘常見類型都提供了方法。這些方法的名稱也比較好記憶,zap.TypeTypebool/int/uint/float64/complex64/time.Time/time.Duration/error等)就表示該類型的字段,zap.Typepp結尾表示該類型指針的字段,zap.Typess結尾表示該類型切片的字段。如:api

  • zap.Bool(key string, val bool) Fieldbool字段
  • zap.Boolp(key string, val *bool) Fieldbool指針字段;
  • zap.Bools(key string, val []bool) Fieldbool切片字段。

固然也有一些特殊類型的字段:緩存

  • zap.Any(key string, value interface{}) Field:任意類型的字段;
  • zap.Binary(key string, val []byte) Field:二進制串的字段。

固然,每一個字段都用方法包一層用起來比較繁瑣。zap也提供了便捷的方法SugarLogger,可使用printf格式符的方式。調用logger.Sugar()便可建立SugaredLoggerSugaredLogger的使用比Logger簡單,只是性能比Logger低 50% 左右,能夠用在非熱點函數中。調用SugarLoggerf結尾的方法與fmt.Printf沒什麼區別,如例子中的Infof。同時SugarLogger還支持以w結尾的方法,這種方式不須要先建立字段對象,直接將字段名和值依次放在參數中便可,如例子中的Infow服務器

默認狀況下,Example輸出的日誌爲 JSON 格式:微信

{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"Failed to fetch URL: http://example.org/api"}

記錄層級關係

前面咱們記錄的日誌都是一層結構,沒有嵌套的層級。咱們可使用zap.Namespace(key string) Field構建一個命名空間,後續的Field都記錄在此命名空間中:app

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  logger.Info("tracked some metrics",
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )

  logger2 := logger.With(
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )
  logger2.Info("tracked some metrics")
}

輸出:

{"level":"info","msg":"tracked some metrics","metrics":{"counter":1}}
{"level":"info","msg":"tracked some metrices","metrics":{"counter":1}}

上面咱們演示了兩種Namespace的用法,一種是直接做爲字段傳入Debug/Info等方法,一種是調用With()建立一個新的Logger,新的Logger記錄日誌時老是帶上預設的字段。With()方法其實是建立了一個新的Logger

// src/go.uber.org/zap/logger.go
func (log *Logger) With(fields ...Field) *Logger {
  if len(fields) == 0 {
    return log
  }
  l := log.clone()
  l.core = l.core.With(fields)
  return l
}

定製Logger

調用NexExample()/NewDevelopment()/NewProduction()這 3 個方法,zap使用默認的配置。咱們也能夠手動調整,配置結構以下:

// src/go.uber.org/zap/config.go
type Config struct {
  Level AtomicLevel `json:"level" yaml:"level"`
  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"`
}
  • Level:日誌級別;
  • Encoding:輸出的日誌格式,默認爲 JSON;
  • OutputPaths:能夠配置多個輸出路徑,路徑能夠是文件路徑和stdout(標準輸出);
  • ErrorOutputPaths:錯誤輸出路徑,也能夠是多個;
  • InitialFields:每條日誌中都會輸出這些值。

其中EncoderConfig爲編碼配置:

// src/go.uber.org/zap/zapcore/encoder.go
type EncoderConfig struct {
  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"`
  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"`
}
  • MessageKey:日誌中信息的鍵名,默認爲msg
  • LevelKey:日誌中級別的鍵名,默認爲level
  • EncodeLevel:日誌中級別的格式,默認爲小寫,如debug/info

調用zap.ConfigBuild()方法便可使用該配置對象建立一個Logger

func main() {
  rawJSON := []byte(`{
    "level":"debug",
    "encoding":"json",
    "outputPaths": ["stdout", "server.log"],
    "errorOutputPaths": ["stderr"],
    "initialFields":{"name":"dj"},
    "encoderConfig": {
      "messageKey": "message",
      "levelKey": "level",
      "levelEncoder": "lowercase"
    }
  }`)

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

  logger.Info("server start work successfully!")
}

上面建立一個輸出到標準輸出stdout和文件server.logLogger。觀察輸出:

{"level":"info","message":"server start work successfully!","name":"dj"}

使用NewDevelopment()建立的Logger使用的是以下的配置:

// src/go.uber.org/zap/config.go
func NewDevelopmentConfig() Config {
  return Config{
    Level:            NewAtomicLevelAt(DebugLevel),
    Development:      true,
    Encoding:         "console",
    EncoderConfig:    NewDevelopmentEncoderConfig(),
    OutputPaths:      []string{"stderr"},
    ErrorOutputPaths: []string{"stderr"},
  }
}

func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
  return zapcore.EncoderConfig{
    // Keys can be anything except the empty string.
    TimeKey:        "T",
    LevelKey:       "L",
    NameKey:        "N",
    CallerKey:      "C",
    MessageKey:     "M",
    StacktraceKey:  "S",
    LineEnding:     zapcore.DefaultLineEnding,
    EncodeLevel:    zapcore.CapitalLevelEncoder,
    EncodeTime:     zapcore.ISO8601TimeEncoder,
    EncodeDuration: zapcore.StringDurationEncoder,
    EncodeCaller:   zapcore.ShortCallerEncoder,
  }
}

NewProduction()的配置可自行查看。

選項

NewExample()/NewDevelopment()/NewProduction()這 3 個函數能夠傳入若干類型爲zap.Option的選項,從而定製Logger的行爲。又一次見到了選項模式!!

zap提供了豐富的選項供咱們選擇。

輸出文件名和行號

調用zap.AddCaller()返回的選項設置輸出文件名和行號。可是有一個前提,必須設置配置對象Config中的CallerKey字段。也所以NewExample()不能輸出這個信息(它的Config沒有設置CallerKey)。

func main() {
  logger, _ := zap.NewProduction(zap.AddCaller())
  defer logger.Sync()

  logger.Info("hello world")
}

輸出:

{"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"hello world"}

Info()方法在main.go的第 9 行被調用。AddCaller()zap.WithCaller(true)等價。

有時咱們稍微封裝了一下記錄日誌的方法,可是咱們但願輸出的文件名和行號是調用封裝函數的位置。這時可使用zap.AddCallerSkip(skip int)向上跳 1 層:

func Output(msg string, fields ...zap.Field) {
  zap.L().Info(msg, fields...)
}

func main() {
  logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1))
  defer logger.Sync()

  zap.ReplaceGlobals(logger)

  Output("hello world")
}

輸出:

{"level":"info","ts":1587740501.5592482,"caller":"skip/main.go:15","msg":"hello world"}

輸出在main函數中調用Output()的位置。若是不指定zap.AddCallerSkip(1),將輸出"caller":"skip/main.go:6",這是在Output()函數中調用zap.Info()的位置。由於這個Output()函數可能在不少地方被調用,因此這個位置參考意義並不大。試試看!

輸出調用堆棧

有時候在某個函數處理中遇到了異常狀況,由於這個函數可能在不少地方被調用。若是咱們能輸出這次調用的堆棧,那麼分析起來就會很方便。咱們可使用zap.AddStackTrace(lvl zapcore.LevelEnabler)達成這個目的。該函數指定lvl和之上的級別都須要輸出調用堆棧:

func f1() {
  f2("hello world")
}

func f2(msg string, fields ...zap.Field) {
  zap.L().Warn(msg, fields...)
}

func main() {
  logger, _ := zap.NewProduction(zap.AddStacktrace(zapcore.WarnLevel))
  defer logger.Sync()

  zap.ReplaceGlobals(logger)

  f1()
}

zapcore.WarnLevel傳入AddStacktrace(),以後Warn()/Error()等級別的日誌會輸出堆棧,Debug()/Info()這些級別不會。運行結果:

{"level":"warn","ts":1587740883.4965692,"caller":"stacktrace/main.go:13","msg":"hello world","stacktrace":"main.f2\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13\nmain.f1\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9\nmain.main\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22\nruntime.main\n\tC:/Go/src/runtime/proc.go:203"}

stacktrace單獨拉出來:

main.f2
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13
  main.f1
  d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9
    main.main
    d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22
      runtime.main
      C:/Go/src/runtime/proc.go:203

很清楚地看到調用路徑。

全局Logger

爲了方便使用,zap提供了兩個全局的Logger,一個是*zap.Logger,可調用zap.L()得到;另外一個是*zap.SugaredLogger,可調用zap.S()得到。須要注意的是,全局的Logger默認並不會記錄日誌!它是一個無實際效果的Logger。看源碼:

// go.uber.org/zap/global.go
var (
  _globalMu sync.RWMutex
  _globalL  = NewNop()
  _globalS  = _globalL.Sugar()
)

咱們可使用ReplaceGlobals(logger *Logger) func()logger設置爲全局的Logger,該函數返回一個無參函數,用於恢復全局Logger設置:

func main() {
  zap.L().Info("global Logger before")
  zap.S().Info("global SugaredLogger before")

  logger := zap.NewExample()
  defer logger.Sync()

  zap.ReplaceGlobals(logger)
  zap.L().Info("global Logger after")
  zap.S().Info("global SugaredLogger after")
}

輸出:

{"level":"info","msg":"global Logger after"}
{"level":"info","msg":"global SugaredLogger after"}

能夠看到在調用ReplaceGlobals以前記錄的日誌並無輸出。

預設日誌字段

若是每條日誌都要記錄一些共用的字段,那麼使用zap.Fields(fs ...Field)建立的選項。例如在服務器日誌中記錄可能都須要記錄serverIdserverName

func main() {
  logger := zap.NewExample(zap.Fields(
    zap.Int("serverId", 90),
    zap.String("serverName", "awesome web"),
  ))

  logger.Info("hello world")
}

輸出:

{"level":"info","msg":"hello world","serverId":90,"serverName":"awesome web"}

與標準日誌庫搭配使用

若是項目一開始使用的是標準日誌庫log,後面想轉爲zap。這時沒必要修改每個文件。咱們能夠調用zap.NewStdLog(l *Logger) *log.Logger返回一個標準的log.Logger,內部實際上寫入的仍是咱們以前建立的zap.Logger

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  std := zap.NewStdLog(logger)
  std.Print("standard logger wrapper")
}

輸出:

{"level":"info","msg":"standard logger wrapper"}

很方便不是嗎?咱們還可使用NewStdLogAt(l *logger, level zapcore.Level) (*log.Logger, error)讓標準接口以level級別寫入內部的*zap.Logger

若是咱們只是想在一段代碼內使用標準日誌庫log,其它地方仍是使用zap.Logger。能夠調用RedirectStdLog(l *Logger) func()。它會返回一個無參函數恢復設置:

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  undo := zap.RedirectStdLog(logger)
  log.Print("redirected standard library")
  undo()

  log.Print("restored standard library")
}

看先後輸出變化:

{"level":"info","msg":"redirected standard library"}
2020/04/24 22:13:58 restored standard library

固然RedirectStdLog也有一個對應的RedirectStdLogAt以特定的級別調用內部的*zap.Logger方法。

總結

zap用在日誌性能和內存分配比較關鍵的地方。本文僅介紹了zap庫的基本使用,子包zapcore中有更底層的接口,能夠定製豐富多樣的Logger

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. zap GitHub:https://github.com/uber-go/zap
  2. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索