1. zap 介绍 Golang Zap库是一个由Uber公司开发的高性能日志记录库,专为Go语言应用程序设计。Zap库以其出色的性能和灵活性而闻名,为开发者提供了结构化、分级别的日志记录功能,此外Zap库还支持日志切割功能,可以根据文件大小、时间或间隔等来切割日志文件,方便管理和分析。Zap库还支持自定义日志格式和输出方式。你可以通过配置Encoder来自定义日志的输出格式,包括时间戳、日志级别、调用文件/函数名和行号等信息。同时,你还可以将日志输出到不同的目的地,如控制台、文件或远程日志系统。
Zap库提供了两种类型的日志记录器:Sugared Logger和Logger。Sugared Logger在性能较好但不建议在关键性的上下文中使用,它相较于其他结构化日志记录包快4-10倍,并支持结构化和printf风格的日志记录。
而Logger则适用于每一微秒和每一次内存分配都至关重要的场景。
Zap库具有一些显著的特点。首先,它提供了极快的日志记录速度,这得益于其内部高效的内存分配和编码机制。其次,Zap库支持结构化日志记录,这意味着你可以将日志记录为键值对的形式,使得日志更加易于理解和分析。此外,Zap库还支持不同的日志级别,如INFO、DEBUG、ERROR等,方便开发者根据需求进行日志级别的调整。
总的来说,Golang Zap库是一个功能强大、性能卓越的日志记录库,适用于各种规模的Go语言应用程序。它提供了灵活的配置选项和丰富的功能,帮助开发者有效地记录和管理日志信息,提高应用程序的可维护性和可观察性。
Zap代码库
2. 快速开始 先安装:
1 2 3 go get go.uber.org/zap
后使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package mainimport ( "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) }
默认情况下,Example输出的日志为 JSON 格式:
1 2 3 4 5 { "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" }
3. 记录层级关系 前面我们记录的日志都是一层结构,没有嵌套的层级。我们可以使用zap.Namespace(key string)
Field构建一个命名空间,后续的Field都记录在此命名空间中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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" ) }
输出:
1 2 3 4 { "level" : "info" , "msg" : "tracked some metrics" , "metrics" : { "counter" : 1 } } { "level" : "info" , "msg" : "tracked some metrices" , "metrics" : { "counter" : 1 } }
上面我们演示了两种Namespace的用法,
一种是直接作为字段传入Debug/Info等方法, 一种是调用With()创建一个新的Logger,新的Logger记录日志时总是带上预设的字段。 With()方法实际上是创建了一个新的Logger:
1 2 3 4 5 6 7 8 9 func (log *Logger) With(fields ...Field) *Logger { if len (fields) == 0 { return log } l := log.clone() l.core = l.core.With(fields) return l }
4. 定制Logger 调用NexExample()/NewDevelopment()/NewProduction()这 3 个方法,zap使用默认的配置。我们也可以手动调整,配置结构如下:
1 2 3 4 5 6 7 8 9 10 11 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为编码配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 / 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。 调用zap.Config的Build()方法即可使用该配置对象创建一个Logger:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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!" ) }
上面创建一个输出到标准输出stdout和文件server.log的Logger。观察输出:
1 2 3 { "level" : "info" , "message" : "server start work successfully!" , "name" : "dj" }
使用NewDevelopment()创建的Logger使用的是如下的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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{ 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()的配置可自行查看。
5. 选项 NewExample()/NewDevelopment()/NewProduction()这 3 个函数可以传入若干类型为zap.Option的选项,从而定制Logger的行为。
zap提供了丰富的选项供我们选择。下面以AddCaller和AddCallerSkip为例进行讲解。
5.1. 输出文件名和行号 调用zap.AddCaller()返回的选项设置输出文件名和行号。但是有一个前提,必须设置配置对象Config中的CallerKey字段。也因此NewExample()不能输出这个信息(它的Config没有设置CallerKey)。
1 2 3 4 5 6 7 8 func main () { logger, _ := zap.NewProduction(zap.AddCaller()) defer logger.Sync() logger.Info("hello world" ) }
输出:
1 { "level" : "info" , "ts" : 1587740198.9508286 , "caller" : "caller/main.go:9" , "msg" : "hello world" }
Info()方法在main.go的第 9 行被调用。AddCaller()与zap.WithCaller(true)等价。
有时我们稍微封装了一下记录日志的方法,但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层:
1 2 3 4 5 6 7 8 9 10 11 12 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" ) }
输出:
1 { "level" : "info" , "ts" : 1587740501.5592482 , "caller" : "skip/main.go:15" , "msg" : "hello world" }
输出在main函数中调用Output()的位置。如果不指定zap.AddCallerSkip(1),将输出”caller”:”skip/main.go:6”,这是在Output()函数中调用zap.Info()的位置。因为这个Output()函数可能在很多地方被调用,所以这个位置参考意义并不大。试试看!
5.2. 输出调用堆栈 有时候在某个函数处理中遇到了异常情况,因为这个函数可能在很多地方被调用。如果我们能输出此次调用的堆栈,那么分析起来就会很方便。我们可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)达成这个目的。该函数指定lvl和之上的级别都需要输出调用堆栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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()这些级别不会。运行结果:
1 2 3 {"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单独拉出来:
1 2 3 4 5 6 7 8 9 10 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
很清楚地看到调用路径。
全局Logger 为了方便使用,zap提供了两个全局的Logger,一个是zap.Logger,可调用zap.L()获得;另一个是 zap.SugaredLogger,可调用zap.S()获得。需要注意的是,全局的Logger默认并不会记录日志!它是一个无实际效果的Logger。看源码:
1 2 3 4 5 6 7 8 var ( _globalMu sync.RWMutex _globalL = NewNop() _globalS = _globalL.Sugar() )
我们可以使用ReplaceGlobals(logger *Logger) func()将logger设置为全局的Logger,该函数返回一个无参函数,用于恢复全局Logger设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 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" ) }
输出:
1 2 3 4 { "level" : "info" , "msg" : "global Logger after" } { "level" : "info" , "msg" : "global SugaredLogger after" }
可以看到在调用ReplaceGlobals之前记录的日志并没有输出。
预设日志字段 如果每条日志都要记录一些共用的字段,那么使用zap.Fields(fs …Field)创建的选项。例如在服务器日志中记录可能都需要记录serverId和serverName:
1 2 3 4 5 6 7 8 func main () { logger := zap.NewExample(zap.Fields( zap.Int("serverId" , 90 ), zap.String("serverName" , "awesome web" ), )) logger.Info("hello world" ) }
输出:
1 { "level" : "info" , "msg" : "hello world" , "serverId" : 90 , "serverName" : "awesome web" }
与标准日志库搭配使用 如果项目一开始使用的是标准日志库log,后面想转为zap。这时不必修改每一个文件。我们可以调用zap.NewStdLog(l *Logger) *log.Logger返回一个标准的log.Logger,内部实际上写入的还是我们之前创建的zap.Logger:
1 2 3 4 5 6 7 8 9 func main () { logger := zap.NewExample() defer logger.Sync() std := zap.NewStdLog(logger) std.Print("standard logger wrapper" ) }
输出:
1 { "level" : "info" , "msg" : "standard logger wrapper" }
很方便不是吗?我们还可以使用NewStdLogAt(l *logger, level zapcore.Level) (log.Logger, error)让标准接口以level级别写入内部的 zap.Logger。
如果我们只是想在一段代码内使用标准日志库log,其它地方还是使用zap.Logger。可以调用RedirectStdLog(l *Logger) func()。它会返回一个无参函数恢复设置:
1 2 3 4 5 6 7 8 9 10 func main () { logger := zap.NewExample() defer logger.Sync() undo := zap.RedirectStdLog(logger) log.Print("redirected standard library" ) undo() log.Print("restored standard library" ) }
看前后输出变化:
1 2 { "level" : "info" , "msg" : "redirected standard library" } 2020 /04 /24 22 : 13 : 58 restored standard library
当然RedirectStdLog也有一个对应的RedirectStdLogAt以特定的级别调用内部的*zap.Logger方法。
总结 zap用在日志性能和内存分配比较关键的地方。本文仅介绍了zap库的基本使用,子包zapcore中有更底层的接口,可以定制丰富多样的Logger。