Go每日一庫(kù)之zap日志庫(kù)的安裝使用指南
簡(jiǎn)介
在很早之前的文章中,我們介紹過(guò) Go 標(biāo)準(zhǔn)日志庫(kù)log和結(jié)構(gòu)化的日志庫(kù)logrus。在熱點(diǎn)函數(shù)中記錄日志對(duì)日志庫(kù)的執(zhí)行性能有較高的要求,不能影響正常邏輯的執(zhí)行時(shí)間。uber
開源的日志庫(kù)zap
,對(duì)性能和內(nèi)存分配做了極致的優(yōu)化。
快速使用
先安裝:
$ go get go.uber.org/zap
后使用:
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
庫(kù)的使用與其他的日志庫(kù)非常相似。先創(chuàng)建一個(gè)logger
,然后調(diào)用各個(gè)級(jí)別的方法記錄日志(Debug/Info/Error/Warn
)。zap
提供了幾個(gè)快速創(chuàng)建logger
的方法,zap.NewExample()
、zap.NewDevelopment()
、zap.NewProduction()
,還有高度定制化的創(chuàng)建方法zap.New()
。創(chuàng)建前 3 個(gè)logger
時(shí),zap
會(huì)使用一些預(yù)定義的設(shè)置,它們的使用場(chǎng)景也有所不同。Example
適合用在測(cè)試代碼中,Development
在開發(fā)環(huán)境中使用,Production
用在生成環(huán)境。
zap
底層 API 可以設(shè)置緩存,所以一般使用defer logger.Sync()
將緩存同步到文件中。
由于fmt.Printf
之類的方法大量使用interface{}
和反射,會(huì)有不少性能損失,并且增加了內(nèi)存分配的頻次。zap
為了提高性能、減少內(nèi)存分配次數(shù),沒(méi)有使用反射,而且默認(rèn)的Logger
只支持強(qiáng)類型的、結(jié)構(gòu)化的日志。必須使用zap
提供的方法記錄字段。zap
為 Go 語(yǔ)言中所有的基本類型和其他常見(jiàn)類型都提供了方法。這些方法的名稱也比較好記憶,zap.Type
(Type
為bool/int/uint/float64/complex64/time.Time/time.Duration/error
等)就表示該類型的字段,zap.Typep
以p
結(jié)尾表示該類型指針的字段,zap.Types
以s
結(jié)尾表示該類型切片的字段。如:
zap.Bool(key string, val bool) Field
:bool
字段zap.Boolp(key string, val *bool) Field
:bool
指針字段;zap.Bools(key string, val []bool) Field
:bool
切片字段。
當(dāng)然也有一些特殊類型的字段:
zap.Any(key string, value interface{}) Field
:任意類型的字段;zap.Binary(key string, val []byte) Field
:二進(jìn)制串的字段。
當(dāng)然,每個(gè)字段都用方法包一層用起來(lái)比較繁瑣。zap
也提供了便捷的方法SugarLogger
,可以使用printf
格式符的方式。調(diào)用logger.Sugar()
即可創(chuàng)建SugaredLogger
。SugaredLogger
的使用比Logger
簡(jiǎn)單,只是性能比Logger
低 50% 左右,可以用在非熱點(diǎn)函數(shù)中。調(diào)用SugarLogger
以f
結(jié)尾的方法與fmt.Printf
沒(méi)什么區(qū)別,如例子中的Infof
。同時(shí)SugarLogger
還支持以w
結(jié)尾的方法,這種方式不需要先創(chuàng)建字段對(duì)象,直接將字段名和值依次放在參數(shù)中即可,如例子中的Infow
。
默認(rèn)情況下,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"}
記錄層級(jí)關(guān)系
前面我們記錄的日志都是一層結(jié)構(gòu),沒(méi)有嵌套的層級(jí)。我們可以使用zap.Namespace(key string) Field
構(gòu)建一個(gè)命名空間,后續(xù)的Field
都記錄在此命名空間中:
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
等方法,一種是調(diào)用With()
創(chuàng)建一個(gè)新的Logger
,新的Logger
記錄日志時(shí)總是帶上預(yù)設(shè)的字段。With()
方法實(shí)際上是創(chuàng)建了一個(gè)新的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
調(diào)用NexExample()/NewDevelopment()/NewProduction()
這 3 個(gè)方法,zap
使用默認(rèn)的配置。我們也可以手動(dòng)調(diào)整,配置結(jié)構(gòu)如下:
// 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
:日志級(jí)別;Encoding
:輸出的日志格式,默認(rèn)為 JSON;OutputPaths
:可以配置多個(gè)輸出路徑,路徑可以是文件路徑和stdout
(標(biāo)準(zhǔn)輸出);ErrorOutputPaths
:錯(cuò)誤輸出路徑,也可以是多個(gè);InitialFields
:每條日志中都會(huì)輸出這些值。
其中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
:日志中信息的鍵名,默認(rèn)為msg
;LevelKey
:日志中級(jí)別的鍵名,默認(rèn)為level
;EncodeLevel
:日志中級(jí)別的格式,默認(rèn)為小寫,如debug/info
。
調(diào)用zap.Config
的Build()
方法即可使用該配置對(duì)象創(chuàng)建一個(gè)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!") }
上面創(chuàng)建一個(gè)輸出到標(biāo)準(zhǔn)輸出stdout
和文件server.log
的Logger
。觀察輸出:
{"level":"info","message":"server start work successfully!","name":"dj"}
使用NewDevelopment()
創(chuàng)建的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()
的配置可自行查看。
選項(xiàng)
NewExample()/NewDevelopment()/NewProduction()
這 3 個(gè)函數(shù)可以傳入若干類型為zap.Option
的選項(xiàng),從而定制Logger
的行為。又一次見(jiàn)到了選項(xiàng)模式??!
zap
提供了豐富的選項(xiàng)供我們選擇。
輸出文件名和行號(hào)
調(diào)用zap.AddCaller()
返回的選項(xiàng)設(shè)置輸出文件名和行號(hào)。但是有一個(gè)前提,必須設(shè)置配置對(duì)象Config
中的CallerKey
字段。也因此NewExample()
不能輸出這個(gè)信息(它的Config
沒(méi)有設(shè)置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 行被調(diào)用。AddCaller()
與zap.WithCaller(true)
等價(jià)。
有時(shí)我們稍微封裝了一下記錄日志的方法,但是我們希望輸出的文件名和行號(hào)是調(diào)用封裝函數(shù)的位置。這時(shí)可以使用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
函數(shù)中調(diào)用Output()
的位置。如果不指定zap.AddCallerSkip(1)
,將輸出"caller":"skip/main.go:6"
,這是在Output()
函數(shù)中調(diào)用zap.Info()
的位置。因?yàn)檫@個(gè)Output()
函數(shù)可能在很多地方被調(diào)用,所以這個(gè)位置參考意義并不大。試試看!
輸出調(diào)用堆棧
有時(shí)候在某個(gè)函數(shù)處理中遇到了異常情況,因?yàn)檫@個(gè)函數(shù)可能在很多地方被調(diào)用。如果我們能輸出此次調(diào)用的堆棧,那么分析起來(lái)就會(huì)很方便。我們可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)
達(dá)成這個(gè)目的。該函數(shù)指定lvl
和之上的級(jí)別都需要輸出調(diào)用堆棧:
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()
等級(jí)別的日志會(huì)輸出堆棧,Debug()/Info()
這些級(jí)別不會(huì)。運(yùn)行結(jié)果:
{"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
單獨(dú)拉出來(lái):
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
很清楚地看到調(diào)用路徑。
全局Logger
為了方便使用,zap
提供了兩個(gè)全局的Logger
,一個(gè)是*zap.Logger
,可調(diào)用zap.L()
獲得;另一個(gè)是*zap.SugaredLogger
,可調(diào)用zap.S()
獲得。需要注意的是,全局的Logger
默認(rèn)并不會(huì)記錄日志!它是一個(gè)無(wú)實(shí)際效果的Logger
??丛创a:
// go.uber.org/zap/global.go var ( _globalMu sync.RWMutex _globalL = NewNop() _globalS = _globalL.Sugar() )
我們可以使用ReplaceGlobals(logger *Logger) func()
將logger
設(shè)置為全局的Logger
,該函數(shù)返回一個(gè)無(wú)參函數(shù),用于恢復(fù)全局Logger
設(shè)置:
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"}
可以看到在調(diào)用ReplaceGlobals
之前記錄的日志并沒(méi)有輸出。
預(yù)設(shè)日志字段
如果每條日志都要記錄一些共用的字段,那么使用zap.Fields(fs ...Field)
創(chuàng)建的選項(xiàng)。例如在服務(wù)器日志中記錄可能都需要記錄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"}
與標(biāo)準(zhǔn)日志庫(kù)搭配使用
如果項(xiàng)目一開始使用的是標(biāo)準(zhǔn)日志庫(kù)log
,后面想轉(zhuǎn)為zap
。這時(shí)不必修改每一個(gè)文件。我們可以調(diào)用zap.NewStdLog(l *Logger) *log.Logger
返回一個(gè)標(biāo)準(zhǔn)的log.Logger
,內(nèi)部實(shí)際上寫入的還是我們之前創(chuàng)建的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)
讓標(biāo)準(zhǔn)接口以level
級(jí)別寫入內(nèi)部的*zap.Logger
。
如果我們只是想在一段代碼內(nèi)使用標(biāo)準(zhǔn)日志庫(kù)log
,其它地方還是使用zap.Logger
??梢哉{(diào)用RedirectStdLog(l *Logger) func()
。它會(huì)返回一個(gè)無(wú)參函數(shù)恢復(fù)設(shè)置:
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
當(dāng)然RedirectStdLog
也有一個(gè)對(duì)應(yīng)的RedirectStdLogAt
以特定的級(jí)別調(diào)用內(nèi)部的*zap.Logger
方法。
總結(jié)
zap
用在日志性能和內(nèi)存分配比較關(guān)鍵的地方。本文僅介紹了zap
庫(kù)的基本使用,子包zapcore
中有更底層的接口,可以定制豐富多樣的Logger
。
大家如果發(fā)現(xiàn)好玩、好用的 Go 語(yǔ)言庫(kù),歡迎到 Go 每日一庫(kù) GitHub 上提交 issue??
參考
- zap GitHub:https://github.com/uber-go/zap
- Go 每日一庫(kù) GitHub:https://github.com/darjun/go-daily-lib
以上就是Go每日一庫(kù)之zap日志庫(kù)的安裝使用指南的詳細(xì)內(nèi)容,更多關(guān)于Go日志庫(kù)zap安裝使用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang實(shí)現(xiàn)PHP數(shù)組特性的方法
我們做業(yè)務(wù)過(guò)程中,對(duì)應(yīng)強(qiáng)類型語(yǔ)言使用有個(gè)痛點(diǎn),就是使用變量之前一定要定義變量類型,那么本文就來(lái)介紹一下golang實(shí)現(xiàn)PHP數(shù)組特性的方法2021-12-12go如何使用gin結(jié)合jwt做登錄功能簡(jiǎn)單示例
jwt全稱Json web token,是一種認(rèn)證和信息交流的工具,這篇文章主要給大家介紹了關(guān)于go如何使用gin結(jié)合jwt做登錄功能的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01Golang中使用Date進(jìn)行日期格式化(沿用Java風(fēng)格)
這篇文章主要介紹了Golang中使用Date進(jìn)行日期格式化,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04淺析go中的map數(shù)據(jù)結(jié)構(gòu)字典
golang中的map是一種數(shù)據(jù)類型,將鍵與值綁定到一起,底層是用哈希表實(shí)現(xiàn)的,可以快速的通過(guò)鍵找到對(duì)應(yīng)的值。這篇文章主要介紹了go中的數(shù)據(jù)結(jié)構(gòu)字典-map,需要的朋友可以參考下2019-11-11go語(yǔ)言中os包的用法實(shí)戰(zhàn)大全
Go在os中提供了文件的基本操作,包括通常意義的打開、創(chuàng)建、讀寫等操作,除此以外為了追求便捷以及性能上,Go還在io/ioutil以及bufio提供一些其他函數(shù)供開發(fā)者使用,這篇文章主要給大家介紹了關(guān)于go語(yǔ)言中os包用法的相關(guān)資料,需要的朋友可以參考下2024-02-02Go?實(shí)現(xiàn)?WebSockets之創(chuàng)建?WebSockets
這篇文章主要介紹了Go?實(shí)現(xiàn)?WebSockets之創(chuàng)建?WebSockets,文章主要探索?WebSockets,并簡(jiǎn)要介紹了它們的工作原理,并仔細(xì)研究了全雙工通信,想了解更多相關(guān)內(nèi)容的小伙伴可以參考一下2022-04-04MacOS下本地golang環(huán)境搭建詳細(xì)教程
這篇文章主要介紹了MacOS下本地golang環(huán)境搭建詳細(xì)教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09go實(shí)現(xiàn)一個(gè)分布式限流器的方法步驟
項(xiàng)目中需要對(duì)api的接口進(jìn)行限流,本文主要介紹了go實(shí)現(xiàn)一個(gè)分布式限流器的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01