Golang定制化zap日志庫(kù)使用過(guò)程分析
前言
本文主要介紹Go語(yǔ)言日志庫(kù)如何簡(jiǎn)易定制化,以及如何在開(kāi)發(fā)中使用。
為什么需要日志
一個(gè)產(chǎn)品的誕生一定是因?yàn)橛行枨?!新技術(shù)大部分都是為了更加便利和實(shí)用而誕生的,日志也不例外。日志顧名思義就是對(duì)整個(gè)項(xiàng)目的事件進(jìn)行記錄。日志可以幫助我們查看某一天中某一時(shí)刻項(xiàng)目的運(yùn)轉(zhuǎn)情況等等。
日志的好處
在日常開(kāi)發(fā)過(guò)程中難免會(huì)遇到BUG出現(xiàn)的情況,日志可以記錄這些BUG出現(xiàn)的地點(diǎn)從而方便進(jìn)行快速定位和排查??梢愿鶕?jù)需求對(duì)日志進(jìn)行自定義的輸出,比如輸出到控制臺(tái)、文件等。日志也可以幫助我們?cè)陂_(kāi)發(fā)過(guò)程中檢測(cè)到程序潛在的問(wèn)題和程序運(yùn)行的流程,能夠有效的提高我們的開(kāi)發(fā)效率。
日志都有什么
要讓程序記錄有效的,便利的日志。** Logger (日志記錄器) 應(yīng)該具備以下特點(diǎn)**:
- 可以將日志信息輸出到控制臺(tái)、文件等地方,輸出到文件便于項(xiàng)目長(zhǎng)久運(yùn)行,輸出到控制臺(tái)有助于開(kāi)發(fā)過(guò)程中檢錯(cuò)的效率。
- 一個(gè)日志應(yīng)該具有多個(gè)基本的級(jí)別,比如
info
,debug
,warn
,error
,fatal
等,他們可以對(duì)日志進(jìn)行分類(lèi)。 - 可以對(duì)日志進(jìn)行切割,按照日志大小、日期、時(shí)間間隔等因素分割。
- 可以手動(dòng)或自動(dòng)記錄一些開(kāi)發(fā)信息。如前端傳入的數(shù)據(jù),異常錯(cuò)誤信息,程序運(yùn)行結(jié)果,錯(cuò)誤行數(shù),日志打印位置等等信息進(jìn)行打印。
Go中默認(rèn)的日志
Go語(yǔ)言中默認(rèn)集成了一個(gè)log
日志庫(kù)
func New(out io.Writer, prefix string, flag int) *Logger { l := &Logger{out: out, prefix: prefix, flag: flag} if out == io.Discard { l.isDiscard = 1 } return l }
使用New
可以獲取到該日志對(duì)象。第一個(gè)參數(shù)為實(shí)現(xiàn)了Writer
接口的對(duì)象。可以使用os.OpenFile()
選擇一個(gè)文件,然后將該文件對(duì)象作為輸出,也可以使用os.Stdout
或os.Stderr
輸出到控制臺(tái)。第二個(gè)參數(shù)需要傳入一個(gè)日志信息每一行的前綴(如果輸出到控制臺(tái)該處可以填空字符串)。第三個(gè)參數(shù)是設(shè)置打印默認(rèn)信息的能力,比如打印時(shí)間等。
測(cè)試日志
var l *log.Logger func main() { l.Printf("main method exec fail, err: %v", errors.New("nil Pointer error")) l.Println("test go log status") l.Fatal("wait five seconds") time.Sleep(time.Second * 5) l.Println("five seconds after!") } func init() { l = log.New(os.Stdout, "[我是一個(gè)前綴]", log.LstdFlags) }
打印信息
[我是一個(gè)前綴]2023/02/10 21:15:22 main method exec fail, err: nil Pointer error
[我是一個(gè)前綴]2023/02/10 21:15:22 test go log status
[我是一個(gè)前綴]2023/02/10 21:15:22 wait five seconds
// Fatal is equivalent to l.Print() followed by a call to os.Exit(1). func (l *Logger) Fatal(v ...any) { l.Output(2, fmt.Sprint(v...)) os.Exit(1) }
在Fatal
之后的程序均不會(huì)執(zhí)行,因?yàn)?code>Fatal執(zhí)行后會(huì)在內(nèi)部調(diào)用os.Exit(1)
,從而在打印結(jié)束后退出進(jìn)程。
goLogger的不足
- 日志級(jí)別只支持
Fatal
,只有一個(gè)Print
函數(shù),沒(méi)有其他級(jí)別 - 日志自定義參數(shù)過(guò)少,無(wú)法打印棧信息,無(wú)法確定請(qǐng)求位置等
Fatal
和Painc
都是執(zhí)行后退出,無(wú)法容忍錯(cuò)誤情況的出現(xiàn)就會(huì)退出程序- 無(wú)法指定輸出格式,只能以文本形式進(jìn)行輸出,沒(méi)有根據(jù)日志大小、時(shí)間間隔、日期進(jìn)行分割的能力
雖然gologger
支持并發(fā),但也只限于簡(jiǎn)單用著還行,實(shí)際開(kāi)發(fā)用起來(lái)并不舒服的情況。
Zap日志庫(kù)
引入日志庫(kù)依賴(lài)
go get -u go.uber.org/zap
zap
日志庫(kù)是Uber
開(kāi)源的。性能很好,因?yàn)椴挥梅瓷鋵?shí)現(xiàn),但需要自己去手動(dòng)指明打印信息的類(lèi)型(下面會(huì)有示例)。個(gè)人覺(jué)得自己指定打印還是挺舒服的。zap
的使用率非常高,不僅支持日志庫(kù)的基本功能,而且很靈活的支持你去進(jìn)一步的封裝或者定制化。zap
支持異步打印。
如何使用zap
格式化配置
func NewDevelopmentEncoderConfig() zapcore.EncoderConfig
func NewProductionEncoderConfig() zapcore.EncoderConfig
func NewProductionConfig() Config
func NewDevelopmentConfig() Config
這里可以根據(jù)實(shí)際生產(chǎn)和測(cè)試環(huán)境需求進(jìn)行選擇,也可以直接使用其他初始化方式。
// NewProductionEncoderConfig returns an opinionated EncoderConfig for // production environments. func NewProductionEncoderConfig() zapcore.EncoderConfig { return zapcore.EncoderConfig{ // 設(shè)置log內(nèi)容里的一些屬性的key TimeKey: "ts",//時(shí)間對(duì)應(yīng)的key名 LevelKey: "level",//日志級(jí)別對(duì)應(yīng)的key名 NameKey: "logger",//logger名對(duì)應(yīng)的key名 CallerKey: "caller",//調(diào)用者對(duì)應(yīng)的key名 FunctionKey: zapcore.OmitKey, MessageKey: "msg",//日志內(nèi)容對(duì)應(yīng)的key名,此參數(shù)必須不為空,否則日志主體不處理 StacktraceKey: "stacktrace",//棧追蹤的key名 // const DefaultLineEnding = "\n" 行末輸出格式 LineEnding: zapcore.DefaultLineEnding, // 日志編碼級(jí)別 EncodeLevel: zapcore.LowercaseLevelEncoder, // 日志時(shí)間解析 EncodeTime: zapcore.EpochTimeEncoder, // 日志日期解析 EncodeDuration: zapcore.SecondsDurationEncoder, // 日志調(diào)用路徑 EncodeCaller: zapcore.ShortCallerEncoder, } }
使用NewProductionEncoderConfig()
創(chuàng)建的 Logger
在記錄日志時(shí)會(huì)自動(dòng)記錄調(diào)用函數(shù)的信息、打日志的時(shí)間,日志級(jí)別等信息。
EncodeLevel
// A LevelEncoder serializes a Level to a primitive type. type LevelEncoder func(Level, PrimitiveArrayEncoder) // 將日志級(jí)別進(jìn)行大寫(xiě)并帶上顏色 func CapitalColorLevelEncoder(l Level, enc PrimitiveArrayEncoder) // 將日志級(jí)別大寫(xiě)不帶顏色 func CapitalLevelEncoder(l Level, enc PrimitiveArrayEncoder) // 將日志級(jí)別小寫(xiě)帶上顏色 func LowercaseColorLevelEncoder(l Level, enc PrimitiveArrayEncoder) // 將日志級(jí)別小寫(xiě)不帶顏色 func LowercaseLevelEncoder(l Level, enc PrimitiveArrayEncoder)
需要實(shí)現(xiàn)LevelEncoder
接口。可以調(diào)整日志編碼級(jí)別,并且選擇帶上或者不帶輸出顏色。
EncodeTime
// A TimeEncoder serializes a time.Time to a primitive type. type TimeEncoder func(time.Time, PrimitiveArrayEncoder) // 根據(jù)不同時(shí)間進(jìn)行格式化 func EpochTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) func EpochMillisTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) func EpochNanosTimeEncoder(t time.Time, enc PrimitiveArrayEncoder)
定制化時(shí)間格式解析,需要實(shí)現(xiàn)TimeEncoder
接口。
EncodeDruation
// A DurationEncoder serializes a time.Duration to a primitive type. type DurationEncoder func(time.Duration, PrimitiveArrayEncoder) // 將日期根據(jù)不同時(shí)間進(jìn)行格式化 func SecondsDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) func NanosDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) func MillisDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) func StringDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder)
定制日期格式解析。需要實(shí)現(xiàn)DruationEncoder
接口
定制化zap
編碼格式
encoderConfig := zap.NewProductionEncoderConfig() // 打印級(jí)別為大寫(xiě) & 彩色 encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // 時(shí)間編碼進(jìn)行指定格式解析 layout -> "[2006-01-02 15:04:05]" encoderConfig.EncodeTime = parseTime(settings.Conf.Layout)
修改日志打印級(jí)別和時(shí)間編碼格式
// parseTime 進(jìn)行時(shí)間格式處理 func parseTime(layout string) zapcore.TimeEncoder { return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { type appendTimeEncoder interface { AppendTimeLayout(time.Time, string) } if enc, ok := enc.(appendTimeEncoder); ok { enc.AppendTimeLayout(t, layout) return } enc.AppendString(t.Format(layout)) } }
實(shí)現(xiàn)zapcore.TimeEncoder
接口,將指定的Layout
參數(shù)進(jìn)行傳入實(shí)現(xiàn)閉包即可。
日志分割
// 日志輸出配置, 借助另外一個(gè)庫(kù) lumberjack 協(xié)助完成日志切割。 lumberjackLogger := &lumberjack.Logger{ Filename: settings.Conf.Filename, // -- 日志文件名 MaxSize: settings.Conf.MaxSize, // -- 最大日志數(shù) M為單位!!! MaxAge: settings.Conf.MaxAge, // -- 最大存在天數(shù) MaxBackups: settings.Conf.MaxBackups, // -- 最大備份數(shù)量 Compress: false, // --是否壓縮 } syncer := zapcore.AddSync(lumberjackLogger)
zap
日志本身不支持日志切割,借助另外一個(gè)庫(kù) lumberjack
協(xié)助完成日志切割。
// -- 用于開(kāi)發(fā)者模式和生產(chǎn)模式之間的切換 var core zapcore.Core if settings.Conf.AppConfig.Mode == "debug" { encoder := zapcore.NewConsoleEncoder(encoderConfig) // 輸出控制臺(tái)編碼格式 core = zapcore.NewTee( zapcore.NewCore(encoder, syncer, zapcore.DebugLevel), // debug級(jí)別打印到日志文件 zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel), // debug級(jí)別打印到控制臺(tái) ) } else { encoder := zapcore.NewJSONEncoder(encoderConfig)// 輸出Json格式,便于日志檢索 core = zapcore.NewCore(encoder, syncer, zapcore.InfoLevel)// info級(jí)別打印到日志文件 } lg := zap.New(core, zap.AddCaller()) // --添加函數(shù)調(diào)用信息
根據(jù)配置信息去選擇具體打印需求。
zap.ReplaceGlobals(lg) // 替換該日志為全局日志 var ( _globalMu sync.RWMutex _globalL = NewNop() ) // L returns the global Logger, which can be reconfigured with ReplaceGlobals. // It's safe for concurrent use. func L() *Logger { _globalMu.RLock() l := _globalL _globalMu.RUnlock() return l }
設(shè)置該日志為全局日志,將原日志進(jìn)行替換,即可在任意位置使用zap.L()
調(diào)用該日志。
完整代碼
// init 初始化日志庫(kù) func init() { encoderConfig := zap.NewProductionEncoderConfig() // 打印級(jí)別為大寫(xiě) & 彩色 encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // 時(shí)間編碼進(jìn)行指定格式解析 encoderConfig.EncodeTime = parseTime(settings.Conf.Layout) // 日志輸出配置, 借助另外一個(gè)庫(kù) lumberjack 協(xié)助完成日志切割。 lumberjackLogger := &lumberjack.Logger{ Filename: settings.Conf.Filename, // -- 日志文件名 MaxSize: settings.Conf.MaxSize, // -- 最大日志數(shù) M為單位!!! MaxAge: settings.Conf.MaxAge, // -- 最大存在天數(shù) MaxBackups: settings.Conf.MaxBackups, // -- 最大備份數(shù)量 Compress: false, // --是否壓縮 } syncer := zapcore.AddSync(lumberjackLogger) // -- 用于開(kāi)發(fā)者模式和生產(chǎn)模式之間的切換 var core zapcore.Core if settings.Conf.AppConfig.Mode == "debug" { encoder := zapcore.NewConsoleEncoder(encoderConfig) core = zapcore.NewTee( zapcore.NewCore(encoder, syncer, zapcore.DebugLevel), zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel), ) } else { encoder := zapcore.NewJSONEncoder(encoderConfig) core = zapcore.NewCore(encoder, syncer, zapcore.InfoLevel) } lg := zap.New(core, zap.AddCaller()) // --添加函數(shù)調(diào)用信息 zap.ReplaceGlobals(lg) // 替換該日志為全局日志 } // parseTime 進(jìn)行時(shí)間格式處理 func parseTime(layout string) zapcore.TimeEncoder { return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { type appendTimeEncoder interface { AppendTimeLayout(time.Time, string) } if enc, ok := enc.(appendTimeEncoder); ok { enc.AppendTimeLayout(t, layout) return } enc.AppendString(t.Format(layout)) } }
測(cè)試日志打印情況
zap.L().Info("test info", zap.String("test String", "ok"), zap.Int("test cnt", 1)) zap.L().Debug("test debug", zap.String("test String", "ok"), zap.Int("test cnt", 2)) zap.L().Error("test error", zap.String("test String", "ok"), zap.Int("test cnt", 3))
[2023-02-10 22:22:17] INFO xxxxx/main.go:22 test info {“test String”: “ok”, “test cnt”: 1}
[2023-02-10 22:22:17] DEBUG xxxxx/main.go:23 test debug {“test String”: “ok”, “test cnt”: 2}
[2023-02-10 22:22:17] ERROR xxxxx/main.go:24 test error {“test String”: “ok”, “test cnt”: 3}
這里就是上述所說(shuō)的自指定類(lèi)型進(jìn)行輸出的情況。
結(jié)合gin框架進(jìn)行使用
雖然gin
框架有自帶的logger
中間件,但我們可以根據(jù)gin
框架實(shí)現(xiàn)的原生日志和異?;謴?fù)中間件進(jìn)行改造并進(jìn)行替換。
Loger
// GinLogger 替換gin中默認(rèn)的logger func GinLogger() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path query := c.Request.URL.RawQuery c.Next() cost := time.Since(start) if c.Writer.Status() != http.StatusOK { // 記錄異常信息 zap.L().Error(query, zap.Int("status", c.Writer.Status()), zap.String("method", c.Request.Method), zap.String("path", path), zap.String("ip", c.ClientIP()), zap.String("user-agent", c.Request.UserAgent()), zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), zap.Duration("cost", cost), ) } }
如果有錯(cuò)誤請(qǐng)求,只要不是狀態(tài)碼為200的全部進(jìn)行打印->狀態(tài)碼、請(qǐng)求方法(get、post…)、路徑、ip、用戶(hù)授權(quán)方、錯(cuò)誤信息、請(qǐng)求花費(fèi)時(shí)間。
// GinRecovery recover掉項(xiàng)目可能出現(xiàn)的panic func GinRecovery(stack bool) gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err != nil { // Check for a broken connection, as it is not really a // condition that warrants a panic stack trace. var brokenPipe bool if ne, ok := err.(*net.OpError); ok { if se, ok := ne.Err.(*os.SyscallError); ok { if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { brokenPipe = true } } } httpRequest, _ := httputil.DumpRequest(c.Request, false) if brokenPipe { zap.L().Error(c.Request.URL.Path, zap.Any("error", err), zap.String("httpRequest", string(httpRequest)), ) // If the connection is dead, we can't write a status to it. c.Error(err.(error)) // nolint: errcheck c.Abort() return } // 這里可以選擇全部打印出來(lái)不必要分割然后循環(huán)輸出 request := strings.Split(string(httpRequest), "\r\n") split := strings.Split(string(debug.Stack()), "\n\t") if stack { zap.L().Error("[Recovery from panic]", zap.Any("error", err)) for _, str := range request { zap.L().Error("[Recovery from request panic]", zap.String("request", str)) } for _, str := range split { zap.L().Error("[Recovery from Stack panic]", zap.String("stack", str)) } } else { zap.L().Error("[Recovery from panic]", zap.Any("error", err)) for _, str := range request { zap.L().Error("[Recovery from request panic]", zap.String("request", str)) } } c.AbortWithStatus(http.StatusInternalServerError) } }() c.Next() } }
這里在Panic
的時(shí)候我采用了分割循環(huán)打印的方法,也可以全部輸出,但是一堆異常情況,不容易看清楚。也可以選擇不打印棧軌跡輸出,只需要在使用recover
中間件時(shí)傳入false
參數(shù)即可。
小結(jié)
zap日志可以靈活的定制時(shí)間、編碼輸出格式、顏色等信息。
zap日志級(jí)別豐富,不利用反射,效率高,但需要手動(dòng)對(duì)類(lèi)型進(jìn)行定義。
到此這篇關(guān)于Golang定制化zap日志庫(kù)使用過(guò)程分析的文章就介紹到這了,更多相關(guān)Go定制化zap日志庫(kù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go項(xiàng)目在GoLand中導(dǎo)入依賴(lài)標(biāo)紅問(wèn)題的解決方案
這篇文章主要介紹了Go項(xiàng)目在GoLand中導(dǎo)入依賴(lài)標(biāo)紅問(wèn)題的解決方案,文中通過(guò)代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-06-06golang實(shí)現(xiàn)文件上傳并轉(zhuǎn)存數(shù)據(jù)庫(kù)功能
這篇文章主要為大家詳細(xì)介紹了golang實(shí)現(xiàn)文件上傳并轉(zhuǎn)存數(shù)據(jù)庫(kù)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07使用GO實(shí)現(xiàn)Paxos共識(shí)算法的方法
這篇文章主要介紹了使用GO實(shí)現(xiàn)Paxos共識(shí)算法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09基于gin的golang web開(kāi)發(fā)之認(rèn)證利器jwt
這篇文章主要介紹了基于gin的golang web開(kāi)發(fā)之認(rèn)證利器jwt,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12手把手教你用VS?code快速搭建一個(gè)Golang項(xiàng)目
Go語(yǔ)言是采用UTF8編碼的,理論上使用任何文本編輯器都能做Go語(yǔ)言開(kāi)發(fā),下面這篇文章主要給大家介紹了關(guān)于使用VS?code快速搭建一個(gè)Golang項(xiàng)目的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04