Go語言中日志統(tǒng)一處理詳解
簡介
在現(xiàn)代軟件開發(fā)中,日志記錄是一項至關(guān)重要的任務(wù),它不僅幫助開發(fā)人員診斷問題,還有助于監(jiān)控和維護應(yīng)用程序。在Go語言中,性能和內(nèi)存分配是至關(guān)重要的考慮因素,因此選擇一個高性能的日志庫非常重要。本文將介紹Uber開源的zap日志庫,它在性能和內(nèi)存分配方面進行了極致的優(yōu)化,成為Go語言中的一種理想選擇。
快速使用
先安裝:
$ 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ù),沒有使用反射,而且默認的Logger
只支持強類型的、結(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
切片字段。
當然也有一些特殊類型的字段:
zap.Any(key string, value interface{}) Field
:任意類型的字段;zap.Binary(key string, val []byte) Field
:二進制串的字段。
當然,每個字段都用方法包一層用起來比較繁瑣。zap
也提供了便捷的方法SugarLogger
,可以使用printf
格式符的方式。調(diào)用logger.Sugar()
即可創(chuàng)建SugaredLogger
。SugaredLogger
的使用比Logger
簡單,只是性能比Logger
低 50% 左右,可以用在非熱點函數(shù)中。調(diào)用SugarLogger
以f
結(jié)尾的方法與fmt.Printf
沒什么區(qū)別,如例子中的Infof
。同時SugarLogger
還支持以w
結(jié)尾的方法,這種方式不需要先創(chuàng)建字段對象,直接將字段名和值依次放在參數(shù)中即可,如例子中的Infow
。
默認情況下,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()
方法實際上是創(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
使用默認的配置。我們也可以手動調(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
:輸出的日志格式,默認為 JSON;OutputPaths
:可以配置多個輸出路徑,路徑可以是文件路徑和stdout
(標準輸出);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
:日志中信息的鍵名,默認為msg
;LevelKey
:日志中級別的鍵名,默認為level
;EncodeLevel
:日志中級別的格式,默認為小寫,如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)建一個輸出到標準輸出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()
的配置可自行查看。
選項
NewExample()/NewDevelopment()/NewProduction()
這 3 個函數(shù)可以傳入若干類型為zap.Option
的選項,從而定制Logger
的行為。又一次見到了選項模式??!
zap
提供了豐富的選項供我們選擇。
輸出文件名和行號
調(diào)用zap.AddCaller()
返回的選項設(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()
的位置。因為這個Output()
函數(shù)可能在很多地方被調(diào)用,所以這個位置參考意義并不大。試試看!
輸出調(diào)用堆棧
有時候在某個函數(shù)處理中遇到了異常情況,因為這個函數(shù)可能在很多地方被調(diào)用。如果我們能輸出此次調(diào)用的堆棧,那么分析起來就會很方便。我們可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)
達成這個目的。該函數(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()
這些級別不會。運行結(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
單獨拉出來:
很清楚地看到調(diào)用路徑。
全局Logger
為了方便使用,zap
提供了兩個全局的Logger
,一個是*zap.Logger
,可調(diào)用zap.L()
獲得;另一個是*zap.SugaredLogger
,可調(diào)用zap.S()
獲得。需要注意的是,全局的Logger
默認并不會記錄日志!它是一個無實際效果的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)建的選項。例如在服務(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"}
與標準日志庫搭配使用
如果項目一開始使用的是標準日志庫log
,后面想轉(zhuǎn)為zap
。這時不必修改每一個文件。我們可以調(diào)用zap.NewStdLog(l *Logger) *log.Logger
返回一個標準的log.Logger
,內(nèi)部實際上寫入的還是我們之前創(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)
讓標準接口以level
級別寫入內(nèi)部的*zap.Logger
。
如果我們只是想在一段代碼內(nèi)使用標準日志庫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
當然RedirectStdLog
也有一個對應(yīng)的RedirectStdLogAt
以特定的級別調(diào)用內(nèi)部的*zap.Logger
方法。
驚喜來啦 通用zap封裝拿走直接用
package common /* Package common provides a logging utility that utilizes the Zap logger library for structured and performant logging. It includes functions for logging at different log levels and is configured to write log entries to a file using the Lumberjack log rotation mechanism. Author: wujiahao Initial Description: This package sets up a structured logging system using Uber's Zap logger and Lumberjack for log rotation. It allows you to log messages at different severity levels, such as Debug, Info, Warn, Error, DPanic, and Fatal, and supports both plain and formatted log messages. The log output is directed to a file with rotation based on size, and each log entry includes a timestamp in ISO8601 format. Additionally, caller information can be included in log entries for debugging purposes. Usage: To use this logging utility, simply import the package and make calls to the logging functions as needed. The logger is initialized with default configuration, but you can customize it by modifying the init() function in this package. */ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) var ( logger *zap.SugaredLogger ) func init() { //log file name fileName :="micro.log" writeSyncer := zapcore.AddSync(&lumberjack.Logger{ Filename: fileName, //file name MaxSize: 521, //the file max size *MB //MaxAge: 0, //the destroy time MaxBackups: 0, //the max back up LocalTime: true, //start local time Compress: true, //is zip }) //encode encoder := zap.NewProductionEncoderConfig() //time format encoder.EncodeTime = zapcore.ISO8601TimeEncoder core := zapcore.NewCore( //encoder zapcore.NewJSONEncoder(encoder), writeSyncer, zap.NewAtomicLevelAt(zap.DebugLevel)) log := zap.New( core, zap.AddCaller(), zap.AddCallerSkip(1))//有時我們稍微封裝了一下記錄日志的方法,但是我們希望輸出的文件名和行號是調(diào)用封裝函數(shù)的位置。這時可以使用zap.AddCallerSkip(skip int)向上跳 1 層: logger= log.Sugar() } func Debug(args ...interface{}) { logger.Debug(args) } func Debugf(template string,args ...interface{}) { logger.Debugf(template, args) } func Info(args ...interface{}) { logger.Info(args...) } func Infof(template string,arg ...interface{}) { logger.Infof(template,arg...) } func Warn(args ...interface{}) { logger.Warn(args...) } func Warnf(template string,args ...interface{}) { logger.Warnf(template,args...) } func Error(args ...interface{}) { logger.Error(args...) } func Errorf(template string,args ...interface{}) { logger.Errorf(template,args) } func DPanic(args ...interface{}) { logger.DPanic(args...) } func DPanicf(template string,args ...interface{}) { logger.DPanicf(template,args...) } func Fatal(args ...interface{}) { logger.Fatal(args...) } func FatalF(tempalte string,args ...interface{}) { logger.Fatalf(tempalte,args...) }
總結(jié)
使用zap
庫,我們可以輕松地實現(xiàn)高性能、結(jié)構(gòu)化的日志記錄,同時減少內(nèi)存分配和性能損失。
本文從安裝、快速使用、配置、全局Logger
等方面介紹了zap
的基本用法。
它的性能和優(yōu)勢使得它成為處理熱點函數(shù)中日志記錄的首選工具。
通過掌握zap
庫,開發(fā)人員可以更好地管理和分析應(yīng)用程序的日志,提高開發(fā)和維護效率,確保應(yīng)用程序的穩(wěn)定性和可維護性。
因此,我們鼓勵開發(fā)人員深入學(xué)習(xí)和應(yīng)用zap
庫,以提高Go
語言應(yīng)用程序的日志記錄質(zhì)量和性能。
以上就是Go語言中日志統(tǒng)一處理詳解的詳細內(nèi)容,更多關(guān)于Go日志處理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang中優(yōu)秀的消息隊列NSQ基礎(chǔ)安裝及使用詳解
這篇文章主要介紹了Golang中優(yōu)秀的消息隊列NSQ基礎(chǔ)安裝及使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12Go語言resty http包調(diào)用jenkins api實例
這篇文章主要為大家介紹了Go語言resty http包調(diào)用jenkins api實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06