在很早以前的文章中,咱們介紹過 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.Type
(Type
爲bool/int/uint/float64/complex64/time.Time/time.Duration/error
等)就表示該類型的字段,zap.Typep
以p
結尾表示該類型指針的字段,zap.Types
以s
結尾表示該類型切片的字段。如:api
zap.Bool(key string, val bool) Field
:bool
字段zap.Boolp(key string, val *bool) Field
:bool
指針字段;zap.Bools(key string, val []bool) Field
:bool
切片字段。固然也有一些特殊類型的字段:緩存
zap.Any(key string, value interface{}) Field
:任意類型的字段;zap.Binary(key string, val []byte) Field
:二進制串的字段。固然,每一個字段都用方法包一層用起來比較繁瑣。zap
也提供了便捷的方法SugarLogger
,可使用printf
格式符的方式。調用logger.Sugar()
便可建立SugaredLogger
。SugaredLogger
的使用比Logger
簡單,只是性能比Logger
低 50% 左右,能夠用在非熱點函數中。調用SugarLogger
以f
結尾的方法與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.Config
的Build()
方法便可使用該配置對象建立一個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.log
的Logger
。觀察輸出:
{"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)
建立的選項。例如在服務器日誌中記錄可能都須要記錄serverId
和serverName
:
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😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~