Golang日志操作庫(kù)zap的使用詳解
一、簡(jiǎn)介
zap 是 uber 開源的一個(gè)高性能,結(jié)構(gòu)化,分級(jí)記錄的日志記錄包。
go1.20.2
zap v1.24.0
zap的特性
- 高性能:zap 對(duì)日志輸出進(jìn)行了多項(xiàng)優(yōu)化以提高它的性能
- 日志分級(jí):有 Debug,Info,Warn,Error,DPanic,Panic,F(xiàn)atal 等
- 日志記錄結(jié)構(gòu)化:日志內(nèi)容記錄是結(jié)構(gòu)化的,比如 json 格式輸出
- 自定義格式:用戶可以自定義輸出的日志格式
- 自定義公共字段:用戶可以自定義公共字段,大家輸出的日志內(nèi)容就共同擁有了這些字段
- 調(diào)試:可以打印文件名、函數(shù)名、行號(hào)、日志時(shí)間等,便于調(diào)試程序
- 自定義調(diào)用棧級(jí)別:可以根據(jù)日志級(jí)別輸出它的調(diào)用棧信息
- Namespace:日志命名空間。定義命名空間后,所有日志內(nèi)容就在這個(gè)命名空間下。命名空間相當(dāng)于一個(gè)文件夾
- 支持 hook 操作
高性能介紹
與其它日志庫(kù)對(duì)比
看github官網(wǎng)的對(duì)比圖,下面的對(duì)比圖來(lái)自:https://github.com/uber-go/zap#performance
Log a message and 10 fields:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
? zap | 2900 ns/op | +0% | 5 allocs/op |
? zap (sugared) | 3475 ns/op | +20% | 10 allocs/op |
zerolog | 10639 ns/op | +267% | 32 allocs/op |
go-kit | 14434 ns/op | +398% | 59 allocs/op |
logrus | 17104 ns/op | +490% | 81 allocs/op |
apex/log | 32424 ns/op | +1018% | 66 allocs/op |
log15 | 33579 ns/op | +1058% | 76 allocs/op |
Log a message with a logger that already has 10 fields of context:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
? zap | 373 ns/op | +0% | 0 allocs/op |
? zap (sugared) | 452 ns/op | +21% | 1 allocs/op |
zerolog | 288 ns/op | -23% | 0 allocs/op |
go-kit | 11785 ns/op | +3060% | 58 allocs/op |
logrus | 19629 ns/op | +5162% | 70 allocs/op |
log15 | 21866 ns/op | +5762% | 72 allocs/op |
apex/log | 30890 ns/op | +8182% | 55 allocs/op |
Log a static string, without any context or printf
-style templating:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
? zap | 381 ns/op | +0% | 0 allocs/op |
? zap (sugared) | 410 ns/op | +8% | 1 allocs/op |
zerolog | 369 ns/op | -3% | 0 allocs/op |
standard library | 385 ns/op | +1% | 2 allocs/op |
go-kit | 606 ns/op | +59% | 11 allocs/op |
logrus | 1730 ns/op | +354% | 25 allocs/op |
apex/log | 1998 ns/op | +424% | 7 allocs/op |
log15 | 4546 ns/op | +1093% | 22 allocs/op |
做了哪些優(yōu)化
基于反射的序列化和字符串格式化,它們都是 CPU 密集型計(jì)算且分配很多小的內(nèi)存。具體到 Go 語(yǔ)言中,使用 encoding/json 和 fmt.Fprintf 格式化 interface{} 會(huì)使程序性能降低。
Zap 咋解決呢?Zap 使用一個(gè)無(wú)反射、零分配的 JOSN 編碼器,基礎(chǔ) Logger 盡可能避免序列化開銷和內(nèi)存分配開銷。在此基礎(chǔ)上,zap 還構(gòu)建了更高級(jí)的 SuggaredLogger。
二、quickstart快速開始
zap 安裝:
go get -u go.uber.org/zap
zap 提供了 2 種日志記錄器:SugaredLogger
和 Logger
。
在需要性能但不是很重要的情況下,使用 SugaredLogger 較合適。它比其它結(jié)構(gòu)化日志包快 4-10 倍,包括 結(jié)構(gòu)化日志和 printf 風(fēng)格的 API。看下面使用 SugaredLogger 例子:
logger, _ := zap.NewProduction() defer logger.Sync() // zap底層有緩沖。在任何情況下執(zhí)行 defer logger.Sync() 是一個(gè)很好的習(xí)慣 sugar := logger.Sugar() sugar.Infow("failed to fetch URL", // 字段是松散類型,不是強(qiáng)類型 "url", url, "attempt", 3, "backoff", time.Second, ) sugar.Infof("Failed to fetch URL: %s", url)
當(dāng)性能和類型安全很重要時(shí),請(qǐng)使用 Logger。它比 SugaredLogger 更快,分配的資源更少,但它只支持結(jié)構(gòu)化日志和強(qiáng)類型字段。
logger, _ := zap.NewProduction() defer logger.Sync() logger.Info("failed to fetch URL", // 字段是強(qiáng)類型,不是松散類型 zap.String("url", url), zap.Int("attempt", 3), zap.Duration("backoff", time.Second), )
三、NewExample/NewDevelopment/NewProduction使用
zap 為我們提供了三種快速創(chuàng)建 logger 的方法: zap.NewProduction()
,zap.NewDevelopment()
,zap.NewExample()
。
見(jiàn)名思義,Example 一般用在測(cè)試代碼中,Development 用在開發(fā)環(huán)境中,Production 用在生成環(huán)境中。這三種方法都預(yù)先設(shè)置好了配置信息。
NewExample()使用
NewExample 構(gòu)建一個(gè) logger,專門為在 zap 的測(cè)試示例使用。它將 DebugLevel 及以上日志用 JSON 格式標(biāo)準(zhǔn)輸出,但它省略了時(shí)間戳和調(diào)用函數(shù),以保持示例輸出的簡(jiǎn)短和確定性。
為什么說(shuō) zap.NewExample()
是 zap 為我們提供快速創(chuàng)建 logger 的方法呢?
因?yàn)樵谶@個(gè)方法里,zap 已經(jīng)定義好了日志配置項(xiàng)部分默認(rèn)值。來(lái)看它的代碼:
// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#L127 func NewExample(options ...Option) *Logger { encoderCfg := zapcore.EncoderConfig{ MessageKey: "msg", // 日志內(nèi)容key:val, 前面的key設(shè)為msg LevelKey: "level", // 日志級(jí)別的key設(shè)為level NameKey: "logger", // 日志名 EncodeLevel: zapcore.LowercaseLevelEncoder, //日志級(jí)別,默認(rèn)小寫 EncodeTime: zapcore.ISO8601TimeEncoder, // 日志時(shí)間 EncodeDuration: zapcore.StringDurationEncoder, } core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), os.Stdout, DebugLevel) return New(core).WithOptions(options...) }
使用例子:
package main import ( "go.uber.org/zap" ) func main() { logger := zap.NewExample() logger.Debug("this is debug message") logger.Info("this is info message") logger.Info("this is info message with fileds", zap.Int("age", 37), zap.String("agender", "man"), ) logger.Warn("this is warn message") logger.Error("this is error message") }
輸出:
{"level":"debug","msg":"this is debug message"} {"level":"info","msg":"this is info message"} {"level":"info","msg":"this is info message with fileds","age":37,"agender":"man"} {"level":"warn","msg":"this is warn message"} {"level":"error","msg":"this is error message"}
NewDevelopment()使用
NewDevelopment() 構(gòu)建一個(gè)開發(fā)使用的 Logger,它以人性化的格式將 DebugLevel 及以上日志信息輸出。它的底層使用
NewDevelopmentConfig().Build(...Option)
構(gòu)建。它的日志格式各種設(shè)置在函數(shù) NewDevelopmentEncoderConfig() 里,想查看詳情設(shè)置,請(qǐng)點(diǎn)進(jìn)去查看。
使用例子:
package main import ( "time" "go.uber.org/zap" ) func main() { logger, _ := zap.NewDevelopment() defer logger.Sync() logger.Info("failed to fetch url", // 強(qiáng)類型字段 zap.String("url", "http://example.com"), zap.Int("attempt", 3), zap.Duration("duration", time.Second), ) logger.With( // 強(qiáng)類型字段 zap.String("url", "http://development.com"), zap.Int("attempt", 4), zap.Duration("duration", time.Second*5), ).Info("[With] failed to fetch url") }
輸出:
2023-03-22T16:02:45.760+0800 INFO zapdemos/newdevelopment1.go:13 failed to fetch url {"url": "http://example.com", "attempt": 3, "duration": "1s"} 2023-03-22T16:02:45.786+0800 INFO zapdemos/newdevelopment1.go:25 [With] failed to fetch url {"url": "http://development.com", "attempt": 4, "duration": "5s"}
上面日志輸出了文件名和行號(hào),NewExample() 沒(méi)有
NewProduction()使用
NewProduction() 構(gòu)建了一個(gè)合理的 Prouction 日志記錄器,它將 info 及以上的日志內(nèi)容以 JSON 格式記寫入標(biāo)準(zhǔn)錯(cuò)誤里。
它的底層使用 NewProductionConfig().Build(...Option)
構(gòu)建。它的日志格式設(shè)置在函數(shù) NewProductionEncoderConfig 里。
使用例子:
package main import ( "time" "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction() defer logger.Sync() url := "http://zap.uber.io" sugar := logger.Sugar() sugar.Infow("failed to fetch URL", "url", url, "attempt", 3, "time", time.Second, ) sugar.Infof("Failed to fetch URL: %s", url) // 或更簡(jiǎn)潔 Sugar() 使用 // sugar := zap.NewProduction().Sugar() // defer sugar.Sync() }
輸出:
{"level":"info","ts":1679472893.2944522,"caller":"zapdemos/newproduction1.go:16","msg":"failed to fetch URL","url":"http://zap.uber.io","attempt":3,"time":1} {"level":"info","ts":1679472893.294975,"caller":"zapdemos/newproduction1.go:22","msg":"Failed to fetch URL: http://zap.uber.io"}
上面日志輸出了文件名和行號(hào),NewExample() 沒(méi)有
使用配置
在這 3 個(gè)函數(shù)中,可以傳入一些配置項(xiàng)。為什么能傳入配置項(xiàng)?我們來(lái)看看 NewExample() 函數(shù)定義:
func NewExample(options ...Option) *Logger
它的函數(shù)傳參有一個(gè) ...Option
選項(xiàng),是一個(gè) interface 類型,它關(guān)聯(lián)的是 Logger struct。只要返回 Option 就可以傳進(jìn) NewExample() 里。在 zap/options.go 文件中可以看到很多返回 Option 的函數(shù),也就是說(shuō)這些函數(shù)都可以傳入 NewExample 函數(shù)里。這里用到了 Go 里面的一個(gè)編碼技巧,函數(shù)選項(xiàng)模式。
zap.Fields() 添加字段到 Logger 中:
package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction(zap.Fields( zap.String("log_name", "testlog"), zap.String("log_author", "prometheus"), )) defer logger.Sync() logger.Info("test fields output") logger.Warn("warn info") }
輸出:
{"level":"info","ts":1679477929.842166,"caller":"zapdemos/fields.go:14","msg":"test fields output","log_name":"testlog","log_author":"prometheus"} {"level":"warn","ts":1679477929.842166,"caller":"zapdemos/fields.go:16","msg":"warn info","log_name":"testlog","log_author":"prometheus"}
zap.Hook() 添加回調(diào)函數(shù):
Hook (鉤子函數(shù))回調(diào)函數(shù)為用戶提供一種簡(jiǎn)單方法,在每次日志內(nèi)容記錄后運(yùn)行這個(gè)回調(diào)函數(shù),執(zhí)行用戶需要的操作。也就是說(shuō)記錄完日志后你還想做其它事情就可以調(diào)用這個(gè)函數(shù)。
package main import ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { logger := zap.NewExample(zap.Hooks(func(entry zapcore.Entry) error { fmt.Println("[zap.Hooks]test Hooks") return nil })) defer logger.Sync() logger.Info("test output") logger.Warn("warn info") }
輸出:
{"level":"info","msg":"test output"} [zap.Hooks]test Hooks {"level":"warn","msg":"warn info"} [zap.Hooks]test Hooks
四、logger和sugaredlogger區(qū)別
從上面例子中看出,zap 有 2 種格式化日志方式:logger 和 sugared logger。
sugared logger:
- 它有很好的性能,比一般日志包快 4-10 倍。
- 支持結(jié)構(gòu)化的日志。
- 支持 printf 風(fēng)格的日志。
- 日志字段不需要定義類型
logger(沒(méi)有sugar)
- 它的性能比 sugared logger 還要快。
- 它只支持強(qiáng)類型的結(jié)構(gòu)化日志。
- 它應(yīng)用在對(duì)性能更加敏感日志記錄中,它的內(nèi)存分配次數(shù)更少。
- 比如如果每一次內(nèi)存分配都很重要的話可以使用這個(gè)。對(duì)類型安全有嚴(yán)格要求也可以使用這個(gè)。
logger 和 sugaredlogger 相互轉(zhuǎn)換:
// 創(chuàng)建 logger logger := zap.NewExample() defer logger.Sync() // 轉(zhuǎn)換 SugaredLogger sugar := logger.Sugar() // 轉(zhuǎn)換 logger plain := sugar.Desugar()
怎么快速構(gòu)建一個(gè) logger 呢?有下面種幾種方法:
- zap.NewProduction()
- zap.NewDevelopment()
- zap.Example()
主要區(qū)別:
- 記錄日志信息和結(jié)構(gòu)不同。
- Example 和 Production 是 json 格式輸出,Development 是普通一行格式輸出,如果后面帶有字段輸出話用json格式。
相同點(diǎn):
- 默認(rèn)情況下都會(huì)打印日志信息到 console 界面
- 都是通過(guò) logger 調(diào)用 Info、Error 等方法
怎么選擇:
- 需要不錯(cuò)的性能但不是很重要的情況下,可以選擇 sugaredlogger。它支持結(jié)構(gòu)化日志和 printf 風(fēng)格的日志記錄。sugaredlogger 的日志記錄是松散類型的,不是強(qiáng)類型,能接受可變數(shù)量的鍵值對(duì)。如果你要用強(qiáng)類型字段記錄,可以使用 SugaredLogger.With 方法。
- 如果是每次或每微秒記錄日志都很重要情況下,可以使用 logger,它比 sugaredlogger 每次分配內(nèi)存更少,性能更高。但它僅支持強(qiáng)類型的結(jié)構(gòu)化日志記錄。
五、自定義配置
快速構(gòu)建 logger 日志記錄器最簡(jiǎn)單的方法就是用 zap 預(yù)定義了配置的方法:NewExample(), NewProduction()
和NewDevelopment()
,這 3 個(gè)方法通過(guò)單個(gè)函數(shù)調(diào)用就可以構(gòu)建一個(gè)日志計(jì)記錄器,也可以簡(jiǎn)單配置。
但是有的項(xiàng)目需要更多的定制,怎么辦?zap 的 Config 結(jié)構(gòu)和 zapcore 的 EncoderConfig 結(jié)構(gòu)可以幫助你,讓你能夠進(jìn)行自定義配置。
配置結(jié)構(gòu)說(shuō)明
Config 配置項(xiàng)源碼:
// zap v1.24.0 type Config struct { // 動(dòng)態(tài)改變?nèi)罩炯?jí)別,在運(yùn)行時(shí)你可以安全改變?nèi)罩炯?jí)別 Level AtomicLevel `json:"level" yaml:"level"` // 將日志記錄器設(shè)置為開發(fā)模式,在 WarnLevel 及以上級(jí)別日志會(huì)包含堆棧跟蹤信息 Development bool `json:"development" yaml:"development"` // 在日志中停止調(diào)用函數(shù)所在文件名、行數(shù) DisableCaller bool `json:"disableCaller" yaml:"disableCaller"` // 完全禁止自動(dòng)堆棧跟蹤。默認(rèn)情況下,在 development 中,warnlevel及以上日志級(jí)別會(huì)自動(dòng)捕獲堆棧跟蹤信息 // 在 production 中,ErrorLevel 及以上也會(huì)自動(dòng)捕獲堆棧信息 DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"` // 設(shè)置采樣策略。沒(méi)有 SamplingConfing 將禁止采樣 Sampling *SamplingConfig `json:"sampling" yaml:"sampling"` // 設(shè)置日志編碼。可以設(shè)置為 console 和 json。也可以通過(guò) RegisterEncoder 設(shè)置第三方編碼格式 Encoding string `json:"encoding" yaml:"encoding"` // 為encoder編碼器設(shè)置選項(xiàng)。詳細(xì)設(shè)置信息在 zapcore.zapcore.EncoderConfig EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"` // 日志輸出地址可以是一個(gè) URLs 地址或文件路徑,可以設(shè)置多個(gè) OutputPaths []string `json:"outputPaths" yaml:"outputPaths"` // 錯(cuò)誤日志輸出地址。默認(rèn)輸出標(biāo)準(zhǔn)錯(cuò)誤信息 ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"` // 可以添加自定義的字段信息到 root logger 中。也就是每條日志都會(huì)攜帶這些字段信息,公共字段 InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"` }
EncoderConfig 結(jié)構(gòu)源碼,它里面也有很多配置選項(xiàng),具體請(qǐng)看 這里:
// zapcore@v1.24.0 type EncoderConfig struct { // 為log entry設(shè)置key。如果 key 為空,那么在日志中的這部分信息也會(huì)省略 MessageKey string `json:"messageKey" yaml:"messageKey"`//日志信息的健名,默認(rèn)為msg LevelKey string `json:"levelKey" yaml:"levelKey"`//日志級(jí)別的健名,默認(rèn)為level TimeKey string `json:"timeKey" yaml:"timeKey"`//記錄日志時(shí)間的健名,默認(rèn)為time 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"` // 日志編碼的一些設(shè)置項(xiàng) EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"` EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` // 與其它編碼器不同, 這個(gè)編碼器可選 EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"` // 配置 interface{} 類型編碼器。如果沒(méi)設(shè)置,將用 json.Encoder 進(jìn)行編碼 NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"` // 配置 console 中字段分隔符。默認(rèn)使用 tab ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"` } type Entry struct { Level Level Time time.Time LoggerName string Message string Caller EntryCaller Stack string }
例子1:基本配置
zap.Config 自定義配置,看官方的一個(gè)基本例子:
package main import ( "encoding/json" "go.uber.org/zap" ) // https://pkg.go.dev/go.uber.org/zap@v1.24.0#hdr-Configuring_Zap func main() { // 表示 zap.Config 的 json 原始編碼 // outputPath: 設(shè)置日志輸出路徑,日志內(nèi)容輸出到標(biāo)準(zhǔn)輸出和文件 logs.log // errorOutputPaths:設(shè)置錯(cuò)誤日志輸出路徑 rawJSON := []byte(`{ "level": "debug", "encoding": "json", "outputPaths": ["stdout", "./logs.log"], "errorOutputPaths": ["stderr"], "initialFields": {"foo": "bar"}, "encoderConfig": { "messageKey": "message-customer", "levelKey": "level", "levelEncoder": "lowercase" } }`) // 把 json 格式數(shù)據(jù)解析到 zap.Config struct var cfg zap.Config if err := json.Unmarshal(rawJSON, &cfg); err != nil { panic(err) } // cfg.Build() 為配置對(duì)象創(chuàng)建一個(gè) Logger // zap.Must() 封裝了 Logger,Must()函數(shù)如果返回值不是 nil,就會(huì)報(bào) panic。也就是檢查Build是否錯(cuò)誤 logger := zap.Must(cfg.Build()) defer logger.Sync() logger.Info("logger construction succeeded") } /* Must() 函數(shù) // var logger = zap.Must(zap.NewProduction()) func Must(logger *Logger, err error) *Logger { if err != nil { panic(err) } return logger } */
consol 輸出如下:
{"level":"info","message-customer":"logger construction succeeded","foo":"bar"}
并且在程序目錄下生成了一個(gè)文件 logs.log,里面記錄的日志內(nèi)容也是上面consol輸出內(nèi)容。每運(yùn)行一次就在日志文件末尾append一次內(nèi)容。
例子2:高級(jí)配置
上面的配置只是基本的自定義配置,如果有一些復(fù)雜的需求,比如在多個(gè)文件之間分割日志。
或者輸出到不是 file 的文件中,比如輸出到 kafka 中,那么就需要使用 zapcore 包。
在下面的例子中,我們將把日志輸出到 kafka 中,并且也輸出到 console 里。并且我們對(duì) kafka 不同主題進(jìn)行編碼設(shè)置,對(duì)輸出到 console 編碼進(jìn)行設(shè)置,也希望處理高優(yōu)先級(jí)的日志。
官方例子:
package main import ( "io" "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { // 首先,定義不同級(jí)別日志處理邏輯 highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel }) lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl < zapcore.ErrorLevel }) // 假設(shè)有2個(gè)kafka 的 topic,一個(gè) debugging,一個(gè) errors // zapcore.AddSync 添加一個(gè)文件句柄。 topicDebugging := zapcore.AddSync(io.Discard) topicErrors := zapcore.AddSync(io.Discard) // 如果他們對(duì)并發(fā)使用不安全,我們可以用 zapcore.Lock 添加一個(gè) mutex 互斥鎖。 consoleDebugging := zapcore.Lock(os.Stdout) consoleErrors := zapcore.Lock(os.Stderr) // 設(shè)置 kafka 和 console 輸出配置 kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) // 把上面的設(shè)置加入到 zapcore.NewCore() 函數(shù)里,然后再把他們加入到 zapcore.NewTee() 函數(shù)里 core := zapcore.NewTee( zapcore.NewCore(kafkaEncoder, topicErrors, highPriority), zapcore.NewCore(consoleEncoder, consoleErrors, highPriority), zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority), zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), ) // 最后調(diào)用 zap.New() 函數(shù) logger := zap.New(core) defer logger.Sync() logger.Info("constructed a logger") }
例子3:日志寫入文件
與上面例子2相似,但是比它簡(jiǎn)單
package main import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { writetofile() } func writetofile() { // 設(shè)置一些配置參數(shù) config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewJSONEncoder(config) defaultLogLevel := zapcore.DebugLevel // 設(shè)置 loglevel logFile, _ := os.OpenFile("./log-test-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 06666) // or os.Create() writer := zapcore.AddSync(logFile) logger := zap.New( zapcore.NewCore(fileEncoder, writer, defaultLogLevel), zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), ) defer logger.Sync() url := "http://www.test.com" logger.Info("write log to file", zap.String("url", url), zap.Int("attemp", 3), ) }
例子4:根據(jù)日志級(jí)別寫入不同文件
這個(gè)與上面例子2相似
package main import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { writeToFileWithLogLevel() } func writeToFileWithLogLevel() { // 設(shè)置配置 config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewJSONEncoder(config) logFile, _ := os.OpenFile("./log-debug-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日志記錄debug信息 errFile, _ := os.OpenFile("./log-err-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日志記錄error信息 teecore := zapcore.NewTee( zapcore.NewCore(fileEncoder, zapcore.AddSync(logFile), zap.DebugLevel), zapcore.NewCore(fileEncoder, zapcore.AddSync(errFile), zap.ErrorLevel), ) logger := zap.New(teecore, zap.AddCaller()) defer logger.Sync() url := "http://www.diff-log-level.com" logger.Info("write log to file", zap.String("url", url), zap.Int("time", 3), ) logger.With( zap.String("url", url), zap.String("name", "jimmmyr"), ).Error("test error ") }
主要是設(shè)置日志級(jí)別,和把 2 個(gè)設(shè)置的 NewCore 放入到方法 NewTee 中。
六、Hook和Namespace
Hook (鉤子函數(shù))回調(diào)函數(shù)為用戶提供一種簡(jiǎn)單方法,在每次日志內(nèi)容記錄后運(yùn)行這個(gè)回調(diào)函數(shù),執(zhí)行用戶需要的操作。也就是說(shuō)記錄完日志后你還想做其它事情就可以調(diào)用這個(gè)函數(shù)。
package main import ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { logger := zap.NewExample(zap.Hooks(func(entry zapcore.Entry) error { fmt.Println("[zap.Hooks]test Hooks") return nil })) defer logger.Sync() logger.Info("test output") logger.Warn("warn info") }
創(chuàng)建一個(gè)命名空間,后面的字段都在這名字空間中。Namespace 就像一個(gè)文件夾,后面文件都放在這個(gè)文件夾里。
package main import ( "go.uber.org/zap" ) func main() { logger := zap.NewExample() defer logger.Sync() logger.Info("some message", zap.Namespace("shop"), zap.String("name", "LiLei"), zap.String("grade", "No2"), ) logger.Error("some error message", zap.Namespace("shop"), zap.String("name", "LiLei"), zap.String("grade", "No3"), ) }
輸出:
{"level":"info","msg":"some message","shop":{"name":"LiLei","grade":"No2"}} {"level":"error","msg":"some error message","shop":{"name":"LiLei","grade":"No3"}}
七、日志切割歸檔
lumberjack 這個(gè)庫(kù)是按照日志大小切割日志文件。
安裝 v2 版本:
go get -u github.com/natefinch/lumberjack@v2
Code:
log.SetOutput(&lumberjack.Logger{ Filename: "/var/log/myapp/foo.log", // 文件位置 MaxSize: 500, // megabytes,M 為單位,達(dá)到這個(gè)設(shè)置數(shù)后就進(jìn)行日志切割 MaxBackups: 3, // 保留舊文件最大份數(shù) MaxAge: 28, //days , 舊文件最大保存天數(shù) Compress: true, // disabled by default,是否壓縮日志歸檔,默認(rèn)不壓縮 })
參照它的文檔和結(jié)合上面自定義配置的例子,寫一個(gè)例子:
package main import ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) func main() { lumberjacklogger := &lumberjack.Logger{ Filename: "./log-rotate-test.json", MaxSize: 1, // megabytes MaxBackups: 3, MaxAge: 28, //days Compress: true, // disabled by default } defer lumberjacklogger.Close() config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder // 設(shè)置時(shí)間格式 fileEncoder := zapcore.NewJSONEncoder(config) core := zapcore.NewCore( fileEncoder, //編碼設(shè)置 zapcore.AddSync(lumberjacklogger), //輸出到文件 zap.InfoLevel, //日志等級(jí) ) logger := zap.New(core) defer logger.Sync() // 測(cè)試分割日志 for i := 0; i < 8000; i++ { logger.With( zap.String("url", fmt.Sprintf("www.test%d.com", i)), zap.String("name", "jimmmyr"), zap.Int("age", 23), zap.String("agradege", "no111-000222"), ).Info("test info ") } }
八、其它方法使用
全局 Logger
zap提供了 2 種全局 Logger,一個(gè)是 zap.Logger,調(diào)用 zap.L() 獲?。?/p>
另外一個(gè)是 zap.SugaredLogger ,調(diào)用 zap.S() 獲取。
注意:直接調(diào)用 zap.L() 或 zap.S() 記錄日志的話,它是不會(huì)記錄任何日志信息。需要調(diào)用 ReplaceGlobals() 函數(shù)將它設(shè)置為全局 Logger。
ReplaceGlobals 替換全局 Logger 和 SugaredLogger,并返回一個(gè)函數(shù)來(lái)恢復(fù)原始值。
并發(fā)使用它是安全的。
看看 zap/global.go 中的源碼:
// https://github.com/uber-go/zap/blob/v1.24.0/global.go var ( _globalMu sync.RWMutex _globalL = NewNop() _globalS = _globalL.Sugar() ) func L() *Logger { _globalMu.RLock() // 加了讀鎖,所以并發(fā)使用是安全的 l := _globalL _globalMu.RUnlock() return l } func S() *SugaredLogger { _globalMu.RLock() // 加了讀鎖,所以并發(fā)使用是安全的 s := _globalS _globalMu.RUnlock() return s } func ReplaceGlobals(logger *Logger) func() { _globalMu.Lock() prev := _globalL _globalL = logger _globalS = logger.Sugar() _globalMu.Unlock() return func() { ReplaceGlobals(prev) } // 返回一個(gè)函數(shù)類型 }
上面源碼中的關(guān)鍵是 _globalL = NewNop() , NewNop 函數(shù)源碼在 zap/logger.go 中,這個(gè)函數(shù)返回初始化了的一個(gè) *Logger:
// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#L85 func NewNop() *Logger { return &Logger{ core: zapcore.NewNopCore(), errorOutput: zapcore.AddSync(io.Discard), addStack: zapcore.FatalLevel + 1, clock: zapcore.DefaultClock, } }
上面是源碼簡(jiǎn)析,下面給出一個(gè)簡(jiǎn)單使用的例子。
簡(jiǎn)單使用例子:
package main import ( "go.uber.org/zap" ) func main() { // 直接調(diào)用是不會(huì)記錄日志信息的,所以下面日志信息不會(huì)輸出 zap.L().Info("no log info") zap.S().Info("no log info [sugared]") logger := zap.NewExample() defer logger.Sync() zap.ReplaceGlobals(logger) // 全局logger,zap.L() 和 zap.S() 需要調(diào)用 ReplaceGlobals 函數(shù)才會(huì)記錄日志信息 zap.L().Info("log info") zap.S().Info("log info [sugared]") }
運(yùn)行輸出:
{"level":"info","msg":"log info"} {"level":"info","msg":"log info [sugared]"}
與標(biāo)準(zhǔn)日志庫(kù)搭配
zap 提供了一個(gè)函數(shù) NewStdLog
,可以把標(biāo)準(zhǔn)日志庫(kù) log 轉(zhuǎn)換為 zap 的日志,這為我們從標(biāo)準(zhǔn)日志庫(kù)轉(zhuǎn)換到 zap 日志庫(kù)的使用提供了簡(jiǎn)潔的轉(zhuǎn)換操作。
例子:
package main import ( "go.uber.org/zap" ) func main() { logger := zap.NewExample() defer logger.Sync() std := zap.NewStdLog(logger) std.Print("standard logger wrapper") }
運(yùn)行輸出:
{"level":"info","msg":"standard logger wrapper"}
如果你還想設(shè)置日志級(jí)別,可以使用另外一個(gè)函數(shù) NewStdLogAt
,它的第二個(gè)參數(shù)就是日志級(jí)別:
NewStdLogAt(l *Logger, level zapcore.Level) (*log.Logger, error)
一段代碼中使用log另外的使用zap
zap 還提供了另外一個(gè)函數(shù) RedirectStdLog
,它可以幫助我們?cè)谝欢未a中使用標(biāo)準(zhǔn)日志庫(kù) log,其它地方還是使用 zap.Logger
。如下例子:
package main import ( "log" "go.uber.org/zap" ) func main() { logger := zap.NewExample() defer logger.Sync() undo := zap.RedirectStdLog(logger) log.Print("redirected standard library") undo() log.Print("this zap logger") }
輸出:
{"level":"info","msg":"redirected standard library"} 2023/05/06 00:47:11 this zap logger
同樣如果想增加日志級(jí)別,可以使用函數(shù) RedirectStdLogAt
:
func RedirectStdLogAt(l *Logger, level zapcore.Level) (func(), error)
輸出調(diào)用堆棧
主要是調(diào)用函數(shù) zap.AddStacktrace()
,見(jiàn)下面例子:
package main import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func Hello() { Warn("hello", zap.String("h", "world"), zap.Int("c", 1)) } func Warn(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) Hello() }
運(yùn)行輸出:
{"level":"warn","ts":1683306442.3578277,"caller":"zapdemos/addstacktrace.go:13","msg":"hello","h":"world","c":1, "stacktrace": "main.Warn\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:13\n main.Hello\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:9\n main.main\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:22\n runtime.main\n\tE:/programfile/go/src/runtime/proc.go:250"}
輸出文件名和行號(hào)
AddCaller 將 Logger 配置為使用 zap 調(diào)用者的文件名、行號(hào)和函數(shù)名稱,把這些信息添加到日志記錄中。它底層調(diào)用的是 WithCaller
。
addcaller.go:
package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction(zap.AddCaller()) defer logger.Sync() logger.Info("AddCaller:line No and filename") }
輸出:
{"level":"info","ts":1683307204.6184027,"caller":"zapdemos/addcaller.go:11","msg":"AddCaller:line No and filename"}
logger.Info() 方法在第11行被調(diào)用。
zap 還提供了另外一個(gè)函數(shù) zap.AddCallerSkip(skip int) Option
,可以設(shè)置向上跳幾層,然后記錄文件名和行號(hào)。向上跳幾層就是跳過(guò)調(diào)用者的數(shù)量。有時(shí)函數(shù)調(diào)用可能有嵌套,用這個(gè)函數(shù)可以定位到里面的函數(shù)。
addcallerskip.go
package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1)) defer logger.Sync() zap.ReplaceGlobals(logger) Hello() } func Hello() { Warn("hello", zap.String("h", "world"), zap.Int("c", 1)) } func Warn(msg string, fields ...zap.Field) { zap.L().Warn(msg, fields...) }
輸出:
{"level":"warn","ts":1683308118.1684704,"caller":"zapdemos/addcallerskip.go:17","msg":"hello","h":"world","c":1}
日志中的 17 表示 Hello() 函數(shù)里的 Warn() 的行號(hào)。
如果 zap.AddCallerSkip(2)
,日志中顯示行號(hào)為 13,表示 Hello() 的行號(hào)。
九、zap使用總結(jié)
- zap 的使用,先創(chuàng)建 logger,再調(diào)用各個(gè)日志級(jí)別方法記錄日志信息。比如 logger.Info()。
- zap 提供了三種快速創(chuàng)建 logger 的方法:
zap.Newproduction()
,zap.NewDevelopment()
,zap.NewExample()
。見(jiàn)名思義,Example 一般用在測(cè)試代碼中,Development 用在開發(fā)環(huán)境中,Production 用在生成環(huán)境中。這三種方法都預(yù)先設(shè)置好了配置信息。它們的日志數(shù)據(jù)類型輸出都是強(qiáng)類型。 - 當(dāng)然,zap 也提供了給用戶自定義的方法
zap.New()
。比如用戶可以自定義一些配置信息等。 - 在上面的例子中,幾乎都有
defer logger.Sync()
這段代碼,為什么?因?yàn)?zap 底層 API 允許緩沖日志以提高性能,在默認(rèn)情況下,日志記錄器是沒(méi)有緩沖的。但是在進(jìn)程退出之前調(diào)用Sync()
方法是一個(gè)好習(xí)慣。 - 如果你在 zap 中使用了 sugaredlogger,把 zap 創(chuàng)建 logger 的三種方法用
logger.Sugar()
包裝下,那么 zap 就支持 printf 風(fēng)格的格式化輸出,也支持以 w 結(jié)尾的方法。如 Infow,Infof 等。這種就是通用類型日志輸出,不是強(qiáng)類型輸出,不需要強(qiáng)制指定輸出的數(shù)據(jù)類型。它們的性能區(qū)別,通用類型會(huì)比強(qiáng)類型下降 50% 左右。
比如 Infow 的輸出形式,Infow 不需要 zap.String 這種指定字段的數(shù)據(jù)類型。如下代碼:
sugar := logger.Sugar() sugar.Infow("failed to fetch URL", "url", url, "attempt", 3, "backoff", time.Second, )
強(qiáng)類型輸出,比如 Info 方法輸出字段和值就需要指定數(shù)據(jù)類型:
logger.Info("failed to fetch url", // 強(qiáng)類型字段 zap.String("url", "http://example.com"), zap.Int("attempt", 3), zap.Duration("backoff", time.Second), )
強(qiáng)類型輸出和通用類型輸出區(qū)別
通用類型輸出,經(jīng)過(guò) interface{} 轉(zhuǎn)換會(huì)有性能損失,標(biāo)準(zhǔn)庫(kù)的 fmt.Printf 為了通用性就用了 interface{} 這種”萬(wàn)能型“的數(shù)據(jù)類型,另外它還使用了反射,性能進(jìn)一步降低。
zap 強(qiáng)類型輸出,zap 為了提供日志輸出性能,zap 的強(qiáng)類型輸出沒(méi)有使用 interface{} 和反射。zap 默認(rèn)輸出就是強(qiáng)類型。
上面介紹,zap 中 3 種創(chuàng)建 logger 方式(zap.Newproduction()
,zap.NewDevelopment()
,zap.NewExample()
)就是強(qiáng)類型日志字段,當(dāng)然,也可以轉(zhuǎn)化為通用類型,用 logger.Sugar()
方法創(chuàng)建 SugaredLogger。
zap.Namespace()
創(chuàng)建一個(gè)命名空間,后面的字段都在這名字空間中。Namespace 就像一個(gè)文件夾,后面文件都放在這個(gè)文件夾里。
logger.Info("some message", zap.Namespace("shop"), zap.String("shopid", "s1234323"), )
{"level":"info","msg":"some message","shop":{"shopid":"s1234323"}}
十、Demo源碼地址
https://github.com/jiujuan/go-exercises/tree/main/zapdemos
以上就是Golang日志操作庫(kù)zap的使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Go日志操作庫(kù)zap的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang 實(shí)現(xiàn)Location跳轉(zhuǎn)方式
這篇文章主要介紹了golang 實(shí)現(xiàn)Location跳轉(zhuǎn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05Golang實(shí)現(xiàn)Json分級(jí)解析及數(shù)字解析實(shí)踐詳解
你是否遇到過(guò)在無(wú)法準(zhǔn)確確定json層級(jí)關(guān)系的情況下對(duì)json進(jìn)行解析的需求呢?本文就來(lái)和大家介紹一次解析不確定的json對(duì)象的經(jīng)歷,以及遇到的問(wèn)題和解決方法2023-02-02go?mod文件內(nèi)容版本號(hào)簡(jiǎn)單用法詳解
這篇文章主要為大家介紹了go?mod文件內(nèi)容版本號(hào)簡(jiǎn)單用法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10