Go語言Zap庫Logger的定制化和封裝使用詳解
前言
日志無論對于程序還是程序員都非常重要,有多重要呢,想要長期在公司健健康康的干下去就得學會階段性劃水,階段性劃水的一大關(guān)鍵的就是干活快過預(yù)期但是裝作。。。不對,這個開頭不對勁,下面重來。
日志無論對于程序還是程序員都非常重要,程序員解決問題的快慢除了經(jīng)驗外,就是看日志能不能有效地記錄問題發(fā)生的現(xiàn)場以及上下文等等。
那么讓讓程序記錄有效的日志,除了程序內(nèi)記日志的點位盡量精準外,還需要有一個稱手的 Logger 。一個好的 Logger (日志記錄器) 要能提供以下這些能力:
- 支持把日志寫入到多個輸出流中,比如可以選擇性的讓測試、開發(fā)環(huán)境同時向控制臺和日志文件輸出日志,生產(chǎn)環(huán)境只輸出到文件中。
- 支持多級別的日志等級,比如常見的有:
TRACE
,DEBUG
,INFO
,WARN
,ERROR
等。 - 支持結(jié)構(gòu)化輸出,結(jié)構(gòu)化輸出現(xiàn)在常用的就是
JSON
形式的,這樣可以讓統(tǒng)一日志平臺,通過 logstash 之類的組件直接把日志聚合到日志平臺上去。 - 需要支持日志切割 --
log rotation
, 按照日期、時間間隔或者文件大小對日志進行切割。 - 在 Log Entry 中(就是每行記錄)除了主動記錄的信息外,還要包括如打印日志的函數(shù)、所在的文件、行號、記錄時間等。
今天我?guī)Т蠹乙黄鹂纯丛趺丛谑褂?Go 語言開發(fā)的項目里打造一個稱手的 Logger,在這之前讓我們先回到 2009 年,看看 Go 語言自誕生之初就提供給我們的內(nèi)置 Logger。
Go 語言原生的Logger
Go 語言自帶 log 內(nèi)置包,為我們提供了一個默認的 Logger,可以直接使用。 這個庫的詳細用法可以在官方的文檔里找到:pkg.go.dev/log
使用 log 記錄日志,默認會輸出到控制臺中。比如下面這個例子:
package main import ( "log" "net/http" ) func main() { simpleHttpGet("www.google.com") simpleHttpGet("https://www.baidu.com") } func simpleHttpGet(url string) { resp, err := http.Get(url) if err != nil { log.Printf("Error fetching url %s : %s", url, err.Error()) } else { log.Printf("Status Code for %s : %s", url, resp.Status) resp.Body.Close() } return }
這個例程中,分別向兩個網(wǎng)址進行 GET 請求,然后記錄了一下返回狀態(tài)碼 / 請求錯誤。 執(zhí)行程序后會有類似輸出:
2022/05/15 15:15:26 Error fetching url www.baidu.com : Get "www.baidu.com": unsupported protocol scheme "" 2022/05/15 15:15:26 Status Code for https://www.baidu.com : 200 OK
因為第一次請求的 URL 中協(xié)議頭缺失, 所以不能成功發(fā)起請求,日志也很好的記錄了錯誤信息。
Go 內(nèi)置的 log 包當然也支持把日志輸出到文件中,通過log.SetOutput
可以把任何 io.Writer
的實現(xiàn)設(shè)置成日志的輸出。下面我們把上面那個例程修改成向文件輸出日志。
大家可以自己試一下運行效果,這里不再做過多演示。
Go 語言原生Logger的缺點
原生 Logger 的優(yōu)點,顯而易見,簡單、開箱即用,不用引用外部的三方庫。我們可以按照開頭處提出的對于一個 Logger 的五個標準再看一下默認Logger 是否能在項目里使用。
- 僅限基本的日志級別
- 只有一個
Print
選項。不支持INFO
/DEBUG
等多個級別。
- 只有一個
- 對于錯誤日志,它有
Fatal
和Panic
- Fatal日志通過調(diào)用
os.Exit(1)
來結(jié)束程序 - Panic日志在寫入日志消息之后拋出一個panic
- 但是它缺少一個
ERROR
日志級別,這個級別可以在不拋出panic
或退出程序的情況下記錄錯誤
- Fatal日志通過調(diào)用
- 缺乏結(jié)構(gòu)化日志格式的能力——只支持簡單文本輸出,不能把日志記錄格式化成
JSON
格式。 - 不提供日志切割的能力。
Zap 日志庫
在 Go 的生態(tài)中,有不少可以選擇的日志庫,之前我們簡單介紹過 logrus
這個庫的使用:點我查看,它與Go的內(nèi)置 log 庫在 api 層面兼容,直接實現(xiàn)了log.Logger
接口,支持把程序的系統(tǒng)級 Logger 切換成它。
不過 logrus 在性能敏感的場景下就顯得不香了,用的更多的是 Uber 開源的 zap 日志庫。由于 Uber 在當今 Go 生態(tài)中的貢獻度很高,加之它本身業(yè)務(wù)—網(wǎng)約車的性能敏感場景,所以 Uber 開源的庫很受歡迎?,F(xiàn)在做項目,使用 Zap 做日志Logger 的非常多。程序員的內(nèi)心OS應(yīng)該是,不管我這并發(fā)高不高,上就完事了,萬一哪天能從2個并發(fā)突然干成 2W 并發(fā)呢。
Zap 性能高的一大原因是:不用反射,日志里每個要寫入的字段都得攜帶著類型
logger.Info( "Success..", zap.String("statusCode", resp.Status), zap.String("url", url))
上面向日志里寫入了一條記錄,Message 是 "Success.." 另外寫入了兩個字符串鍵值對。 Zap 針對日志里要寫入的字段,每個類型都有一個對應(yīng)的方法把字段轉(zhuǎn)成 zap.Field
類型 。比如:
zap.Int('key', 123) zap.Bool('key', true) zap.Error('err', err) zap.Any('arbitraryType', &User{})
還有很多中這種類型方法,就不一一列舉啦。這種記錄日志的方式造成在使用體驗上稍稍有點差,不過考慮到性能上收益這點使用體驗上的損失也能接受。
下面我們先來學習一下 Zap 的使用方法,再對項目中使用 Zap 時做些自定義的配置和封裝,讓它變得更好用,最重要的是匹配上我們開頭提出的關(guān)于好的 Logger 的五條標準。
Zap 的使用方法
安裝zap
首先說一下,zap 的安裝方式,直接運行以下命令下載 zap 到本地的依賴庫中。
go get -u go.uber.org/zap
設(shè)置 Logger
我們先說 zap 提供的配置好的 Logger ,稍后會對它進行自定義。
- 通過調(diào)用
zap.NewProduction()
、zap.NewDevelopment()
、zap.Example()
這三個方法,都可以創(chuàng)建 Logger。 - 上面三個方法都可以創(chuàng)建 Logger,他們都對 Logger 進行了不同的配置,比如
zap.NewProduction()
創(chuàng)建的 Logger 在記錄日志時會自動記錄調(diào)用函數(shù)的信息、打日志的時間等,這三個不用糾結(jié),直接都用zap.NewProduction()
,且在項目中使用的時候,我們不會直接用 zap 配置好的 Logger ,需要再做更細致的定制。
zap 的 Logger 提供了記錄不同等級的日志的方法,像從低到高的日志等級一般有:Debug、Info、Warn、Error 這些級別都有對應(yīng)的方法。他們的使用方式都一樣,下面是 Info 方法的方法簽名。
func (log *Logger) Info(msg string, fields ...Field) { if ce := log.check(InfoLevel, msg); ce != nil { ce.Write(fields...) } }
方法的第一個參數(shù)是日志里 msg
字段要記錄的信息,msg
是日志行記錄里一個固定的字段,要再添加其他字段到日志,直接傳遞 zap.Field
類型的參數(shù)即可,上面我們已經(jīng)說過zap.Field
類型的字段,就是由 zap.String("key", "value")
這類方法創(chuàng)建出來的。由于 Info 方法簽名里 fileds
參數(shù)聲明是可變參數(shù),所以支持添加任意多個字段到日志行記錄里, 比如例程里的:
logger.Info("Success..", zap.String("statusCode", resp.Status), zap.String("url", url))
即日志行記錄里,除了 msg
字段,還添加了statusCode
,url
兩個自定義字段。 上面例程里使用的zap.NewProduction()
創(chuàng)建的 Logger 會向控制臺輸出JSON
格式的日志行,比如上面使用Info
方法后,控制臺會有類似下面的輸出。
{"level":"info","ts":1558882294.665447,"caller":"basiclogger/UberGoLogger.go:31","msg":"Success..","statusCode":"200 OK","url":"https://www.baidu.com"}
定制 Zap 的 Logger
下面我們把 zap 做進一步的自定義配置,讓日志不光能輸出到控制臺,也能輸出到文件,再把日志時間由時間戳格式,換成更容易被人類看懂的DateTime
時間格式。
下面少說話,直接上代碼,必要的解釋放在了注釋里。
var logger *zap.Logger func init() { encoderConfig := zap.NewProductionEncoderConfig() // 設(shè)置日志記錄中時間的格式 encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 日志Encoder 還是JSONEncoder,把日志行格式化成JSON格式的 encoder := zapcore.NewJSONEncoder(encoderConfig) file, _ := os.OpenFile("/tmp/test.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 644) fileWriteSyncer = zapcore.AddSync(file) core := zapcore.NewTee( // 同時向控制臺和文件寫日志, 生產(chǎn)環(huán)境記得把控制臺寫入去掉,日志記錄的基本是Debug 及以上,生產(chǎn)環(huán)境記得改成Info zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel), zapcore.NewCore(encoder, fileWriteSyncer, zapcore.DebugLevel), ) logger = zap.New(core) }
日志切割
Zap 本身不支持日志切割,可以借助另外一個庫 lumberjack 協(xié)助完成切割。
func getFileLogWriter() (writeSyncer zapcore.WriteSyncer) { // 使用 lumberjack 實現(xiàn) log rotate lumberJackLogger := &lumberjack.Logger{ Filename: "/tmp/test.log", MaxSize: 100, // 單個文件最大100M MaxBackups: 60, // 多于 60 個日志文件后,清理較舊的日志 MaxAge: 1, // 一天一切割 Compress: false, } return zapcore.AddSync(lumberJackLogger) }
封裝 Logger
我們不能每次使用日志,都這么設(shè)置一番,所以最好的還是把這些配置初始化放在一個單獨的包里,這樣在項目中初始化一次即可。
除了上面的那些配置外,我們的配置里還少了些日志調(diào)用方的信息,比如函數(shù)名、文件位置、行號等,這樣在排查問題看日志的時候,定位問題的時效會提高不少。
我們對 Logger 再做一下封裝。
// 發(fā)送私信 go-logger 給公眾號「網(wǎng)管叨bi叨」 // 可獲得完整代碼和使用Demo package zlog // 簡單封裝一下對 zap 日志庫的使用 // 使用方式: // zlog.Debug("hello", zap.String("name", "Kevin"), zap.Any("arbitraryObj", dummyObject)) // zlog.Info("hello", zap.String("name", "Kevin"), zap.Any("arbitraryObj", dummyObject)) // zlog.Warn("hello", zap.String("name", "Kevin"), zap.Any("arbitraryObj", dummyObject)) var logger *zap.Logger func init() { ...... } func getFileLogWriter() (writeSyncer zapcore.WriteSyncer) { ...... } func Info(message string, fields ...zap.Field) { callerFields := getCallerInfoForLog() fields = append(fields, callerFields...) logger.Info(message, fields...) } func Debug(message string, fields ...zap.Field) { callerFields := getCallerInfoForLog() fields = append(fields, callerFields...) logger.Debug(message, fields...) } func Error(message string, fields ...zap.Field) { callerFields := getCallerInfoForLog() fields = append(fields, callerFields...) logger.Error(message, fields...) } func Warn(message string, fields ...zap.Field) { callerFields := getCallerInfoForLog() fields = append(fields, callerFields...) logger.Warn(message, fields...) } func getCallerInfoForLog() (callerFields []zap.Field) { pc, file, line, ok := runtime.Caller(2) // 回溯兩層,拿到寫日志的調(diào)用方的函數(shù)信息 if !ok { return } funcName := runtime.FuncForPC(pc).Name() funcName = path.Base(funcName) //Base函數(shù)返回路徑的最后一個元素,只保留函數(shù)名 callerFields = append(callerFields, zap.String("func", funcName), zap.String("file", file), zap.Int("line", line)) return }
為啥不用 zap.New(core, zap.AddCaller())
這種方式,在日志行里添加調(diào)用方的信息呢?主要還是想更靈活點,能自己制定對應(yīng)的日志字段,所以把 Caller
的幾個信息放到單獨的字段里,等把日志收集到日志平臺上去后,查詢?nèi)罩镜臅r候也更利于檢索。
在下面的例程中嘗試使用我們封裝好的日志 Logger 做個簡單的測試。
package main import ( "example.com/utils/zlog" ) type User strunct { Name stirng } func main() { user := &User{ "Name": "Kevin" } zlog.Info("test log", zap.Any("user", user)) }
輸出類似下面的輸出。
{"level":"info","ts":"2022-05-15T21:22:22.687+0800","msg":"test log","res":{"Name":"Kevin"},"func":"main.Main","file":"/Users/Kevin/go/src/example.com/demo/zap.go","line":84}
總結(jié)
關(guān)于 Zap Logger 的定制化和封裝,這里只是舉了一些基本又必要的入門級定制化,等大家掌握后,可以參照官方文檔提供的接口進行更多定制化。
源碼鏈接 https://github.com/go-study-lab/go-http-server/blob/master/utils/zlog/log.go
更多關(guān)于Go語言Zap庫Logger定制化封裝的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang?WebSocket創(chuàng)建單獨會話詳細實例
這篇文章主要給大家介紹了關(guān)于Golang?WebSocket創(chuàng)建單獨會話的相關(guān)資料,WebSocket 協(xié)議主要為了解決基于 HTTP/1.x 的 Web 應(yīng)用無法實現(xiàn)服務(wù)端向客戶端主動推送的問題,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2023-11-11Go語言性能監(jiān)控和調(diào)優(yōu)的工具和方法
本文介紹了Go語言性能監(jiān)控和調(diào)優(yōu)的工具和方法,包括?pprof、expvar?和?trace?等工具的使用方法和注意事項,以及性能調(diào)優(yōu)的一些常見方法,如減少內(nèi)存分配、避免頻繁的垃圾回收、避免過度查詢數(shù)據(jù)庫等,針對不同的程序,應(yīng)該根據(jù)實際情況采用不同的優(yōu)化方法2024-01-01Go結(jié)構(gòu)體指針引發(fā)的值傳遞思考分析
這篇文章主要為大家介紹了Go結(jié)構(gòu)體指針引發(fā)的值傳遞思考分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12