Go每日一庫之zap日志庫的安裝使用指南
簡介
在很早之前的文章中,我們介紹過 Go 標(biāo)準(zhǔn)日志庫log和結(jié)構(gòu)化的日志庫logrus。在熱點(diǎn)函數(shù)中記錄日志對日志庫的執(zhí)行性能有較高的要求,不能影響正常邏輯的執(zhí)行時間。uber開源的日志庫zap,對性能和內(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庫的使用與其他的日志庫非常相似。先創(chuàng)建一個logger,然后調(diào)用各個級別的方法記錄日志(Debug/Info/Error/Warn)。zap提供了幾個快速創(chuàng)建logger的方法,zap.NewExample()、zap.NewDevelopment()、zap.NewProduction(),還有高度定制化的創(chuàng)建方法zap.New()。創(chuàng)建前 3 個logger時,zap會使用一些預(yù)定義的設(shè)置,它們的使用場景也有所不同。Example適合用在測試代碼中,Development在開發(fā)環(huán)境中使用,Production用在生成環(huán)境。
zap底層 API 可以設(shè)置緩存,所以一般使用defer logger.Sync()將緩存同步到文件中。
由于fmt.Printf之類的方法大量使用interface{}和反射,會有不少性能損失,并且增加了內(nèi)存分配的頻次。zap為了提高性能、減少內(nèi)存分配次數(shù),沒有使用反射,而且默認(rèn)的Logger只支持強(qiáng)類型的、結(jié)構(gòu)化的日志。必須使用zap提供的方法記錄字段。zap為 Go 語言中所有的基本類型和其他常見類型都提供了方法。這些方法的名稱也比較好記憶,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)然,每個字段都用方法包一層用起來比較繁瑣。zap也提供了便捷的方法SugarLogger,可以使用printf格式符的方式。調(diào)用logger.Sugar()即可創(chuàng)建SugaredLogger。SugaredLogger的使用比Logger簡單,只是性能比Logger低 50% 左右,可以用在非熱點(diǎn)函數(shù)中。調(diào)用SugarLogger以f結(jié)尾的方法與fmt.Printf沒什么區(qū)別,如例子中的Infof。同時SugarLogger還支持以w結(jié)尾的方法,這種方式不需要先創(chuàng)建字段對象,直接將字段名和值依次放在參數(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"}記錄層級關(guān)系
前面我們記錄的日志都是一層結(jié)構(gòu),沒有嵌套的層級。我們可以使用zap.Namespace(key string) Field構(gòu)建一個命名空間,后續(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)建一個新的Logger,新的Logger記錄日志時總是帶上預(yù)設(shè)的字段。With()方法實(shí)際上是創(chuàng)建了一個新的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 個方法,zap使用默認(rèn)的配置。我們也可以手動調(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:日志級別;Encoding:輸出的日志格式,默認(rèn)為 JSON;OutputPaths:可以配置多個輸出路徑,路徑可以是文件路徑和stdout(標(biāo)準(zhǔn)輸出);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:日志中信息的鍵名,默認(rèn)為msg;LevelKey:日志中級別的鍵名,默認(rèn)為level;EncodeLevel:日志中級別的格式,默認(rèn)為小寫,如debug/info。
調(diào)用zap.Config的Build()方法即可使用該配置對象創(chuàng)建一個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)建一個輸出到標(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 個函數(shù)可以傳入若干類型為zap.Option的選項(xiàng),從而定制Logger的行為。又一次見到了選項(xiàng)模式!!
zap提供了豐富的選項(xiàng)供我們選擇。
輸出文件名和行號
調(diào)用zap.AddCaller()返回的選項(xiàng)設(shè)置輸出文件名和行號。但是有一個前提,必須設(shè)置配置對象Config中的CallerKey字段。也因此NewExample()不能輸出這個信息(它的Config沒有設(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)等價。
有時我們稍微封裝了一下記錄日志的方法,但是我們希望輸出的文件名和行號是調(diào)用封裝函數(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)檫@個Output()函數(shù)可能在很多地方被調(diào)用,所以這個位置參考意義并不大。試試看!
輸出調(diào)用堆棧
有時候在某個函數(shù)處理中遇到了異常情況,因?yàn)檫@個函數(shù)可能在很多地方被調(diào)用。如果我們能輸出此次調(diào)用的堆棧,那么分析起來就會很方便。我們可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)達(dá)成這個目的。該函數(shù)指定lvl和之上的級別都需要輸出調(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()等級別的日志會輸出堆棧,Debug()/Info()這些級別不會。運(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ú)拉出來:
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提供了兩個全局的Logger,一個是*zap.Logger,可調(diào)用zap.L()獲得;另一個是*zap.SugaredLogger,可調(diào)用zap.S()獲得。需要注意的是,全局的Logger默認(rèn)并不會記錄日志!它是一個無實(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ù)返回一個無參函數(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之前記錄的日志并沒有輸出。
預(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)日志庫搭配使用
如果項(xiàng)目一開始使用的是標(biāo)準(zhǔn)日志庫log,后面想轉(zhuǎn)為zap。這時不必修改每一個文件。我們可以調(diào)用zap.NewStdLog(l *Logger) *log.Logger返回一個標(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級別寫入內(nèi)部的*zap.Logger。
如果我們只是想在一段代碼內(nèi)使用標(biāo)準(zhǔn)日志庫log,其它地方還是使用zap.Logger。可以調(diào)用RedirectStdLog(l *Logger) func()。它會返回一個無參函數(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也有一個對應(yīng)的RedirectStdLogAt以特定的級別調(diào)用內(nèi)部的*zap.Logger方法。
總結(jié)
zap用在日志性能和內(nèi)存分配比較關(guān)鍵的地方。本文僅介紹了zap庫的基本使用,子包zapcore中有更底層的接口,可以定制豐富多樣的Logger。
大家如果發(fā)現(xiàn)好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??
參考
- zap GitHub:https://github.com/uber-go/zap
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
以上就是Go每日一庫之zap日志庫的安裝使用指南的詳細(xì)內(nèi)容,更多關(guān)于Go日志庫zap安裝使用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang實(shí)現(xiàn)PHP數(shù)組特性的方法
我們做業(yè)務(wù)過程中,對應(yīng)強(qiáng)類型語言使用有個痛點(diǎn),就是使用變量之前一定要定義變量類型,那么本文就來介紹一下golang實(shí)現(xiàn)PHP數(shù)組特性的方法2021-12-12
Golang中使用Date進(jìn)行日期格式化(沿用Java風(fēng)格)
這篇文章主要介紹了Golang中使用Date進(jìn)行日期格式化,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04
淺析go中的map數(shù)據(jù)結(jié)構(gòu)字典
golang中的map是一種數(shù)據(jù)類型,將鍵與值綁定到一起,底層是用哈希表實(shí)現(xiàn)的,可以快速的通過鍵找到對應(yīng)的值。這篇文章主要介紹了go中的數(shù)據(jù)結(jié)構(gòu)字典-map,需要的朋友可以參考下2019-11-11
Go?實(shí)現(xiàn)?WebSockets之創(chuàng)建?WebSockets
這篇文章主要介紹了Go?實(shí)現(xiàn)?WebSockets之創(chuàng)建?WebSockets,文章主要探索?WebSockets,并簡要介紹了它們的工作原理,并仔細(xì)研究了全雙工通信,想了解更多相關(guān)內(nèi)容的小伙伴可以參考一下2022-04-04
MacOS下本地golang環(huán)境搭建詳細(xì)教程
這篇文章主要介紹了MacOS下本地golang環(huán)境搭建詳細(xì)教程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09

