内容简介:在很早之前的文章中,我们介绍过 Go 标准日志库先安装:后使用:
简介
在很早之前的文章中,我们介绍过 Go 标准日志库 log
和结构化的日志库 logrus
。在热点函数中记录日志对日志库的执行性能有较高的要求,不能影响正常逻辑的执行时间。 uber
开源的日志库 zap
,对性能和内存分配做了极致的优化。
快速使用
先安装:
$ 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
库的使用与其他的日志库非常相似。先创建一个 logger
,然后调用各个级别的方法记录日志( Debug/Info/Error/Warn
)。 zap
提供了几个快速创建 logger
的方法, zap.NewExample()
、 zap.NewDevelopment()
、 zap.NewProduction()
,还有高度定制化的创建方法 zap.New()
。创建前 3 个 logger
时, zap
会使用一些预定义的设置,它们的使用场景也有所不同。 Example
适合用在测试代码中, Development
在开发环境中使用, Production
用在生成环境。
zap
底层 API 可以设置缓存,所以一般使用 defer logger.Sync()
将缓存同步到文件中。
由于 fmt.Printf
之类的方法大量使用 interface{}
和反射,会有不少性能损失,并且增加了内存分配的频次。 zap
为了提高性能、减少内存分配次数,没有使用反射,而且默认的 Logger
只支持强类型的、结构化的日志。必须使用 zap
提供的方法记录字段。 zap
为 Go 语言中所有的基本类型和其他常见类型都提供了方法。这些方法的名称也比较好记忆, zap.Type
( Type
为 bool/int/uint/float64/complex64/time.Time/time.Duration/error
等)就表示该类型的字段, zap.Typep
以 p
结尾表示该类型指针的字段, zap.Types
以 s
结尾表示该类型切片的字段。如:
-
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
格式符的方式。调用 logger.Sugar()
即可创建 SugaredLogger
。 SugaredLogger
的使用比 Logger
简单,只是性能比 Logger
低 50% 左右,可以用在非热点函数中。调用 SugarLogger
以 f
结尾的方法与 fmt.Printf
没什么区别,如例子中的 Infof
。同时 SugarLogger
还支持以 w
结尾的方法,这种方式不需要先创建字段对象,直接将字段名和值依次放在参数中即可,如例子中的 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"}
记录层级关系
前面我们记录的日志都是一层结构,没有嵌套的层级。我们可以使用 zap.Namespace(key string) Field
构建一个 命名空间 ,后续的 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
等方法,一种是调用 With()
创建一个新的 Logger
,新的 Logger
记录日志时总是带上预设的字段。 With()
方法实际上是创建了一个新的 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
调用 NexExample()/NewDevelopment()/NewProduction()
这 3 个方法, zap
使用默认的配置。我们也可以手动调整,配置结构如下:
// 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
。
调用 zap.Config
的 Build()
方法即可使用该配置对象创建一个 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!") }
上面创建一个输出到标准输出 stdout
和文件 server.log
的 Logger
。观察输出:
{"level":"info","message":"server start work successfully!","name":"dj"}
使用 NewDevelopment()
创建的 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 个函数可以传入若干类型为 zap.Option
的选项,从而定制 Logger
的行为。又一次见到了 选项模式 !!
zap
提供了丰富的选项供我们选择。
输出文件名和行号
调用 zap.AddCaller()
返回的选项设置输出文件名和行号。但是有一个前提,必须设置配置对象 Config
中的 CallerKey
字段。也因此 NewExample()
不能输出这个信息(它的 Config
没有设置 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 行被调用。 AddCaller()
与 zap.WithCaller(true)
等价。
有时我们稍微封装了一下记录日志的方法,但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用 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
函数中调用 Output()
的位置。如果不指定 zap.AddCallerSkip(1)
,将输出 "caller":"skip/main.go:6"
,这是在 Output()
函数中调用 zap.Info()
的位置。因为这个 Output()
函数可能在很多地方被调用,所以这个位置参考意义并不大。试试看!
输出调用堆栈
有时候在某个函数处理中遇到了异常情况,因为这个函数可能在很多地方被调用。如果我们能输出此次调用的堆栈,那么分析起来就会很方便。我们可以使用 zap.AddStackTrace(lvl zapcore.LevelEnabler)
达成这个目的。该函数指定 lvl
和之上的级别都需要输出调用堆栈:
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()
这些级别不会。运行结果:
{"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
单独拉出来:
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
。看源码:
// go.uber.org/zap/global.go var ( _globalMu sync.RWMutex _globalL = NewNop() _globalS = _globalL.Sugar() )
我们可以使用 ReplaceGlobals(logger *Logger) func()
将 logger
设置为全局的 Logger
,该函数返回一个无参函数,用于恢复全局 Logger
设置:
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"}
可以看到在调用 ReplaceGlobals
之前记录的日志并没有输出。
预设日志字段
如果每条日志都要记录一些共用的字段,那么使用 zap.Fields(fs ...Field)
创建的选项。例如在服务器日志中记录可能都需要记录 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
,后面想转为 zap
。这时不必修改每一个文件。我们可以调用 zap.NewStdLog(l *Logger) *log.Logger
返回一个标准的 log.Logger
,内部实际上写入的还是我们之前创建的 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
级别写入内部的 *zap.Logger
。
如果我们只是想在一段代码内使用标准日志库 log
,其它地方还是使用 zap.Logger
。可以调用 RedirectStdLog(l *Logger) func()
。它会返回一个无参函数恢复设置:
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
也有一个对应的 RedirectStdLogAt
以特定的级别调用内部的 *zap.Logger
方法。
总结
zap
用在日志性能和内存分配比较关键的地方。本文仅介绍了 zap
库的基本使用,子包 zapcore
中有更底层的接口,可以定制丰富多样的 Logger
。
大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue:smile:
参考
- zap GitHub: https://github.com/uber-go/zap
- Go 每日一库 GitHub: https://github.com/darjun/go-daily-lib
我
我的博客: https://darjun.github.io
欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~
欢迎关注我们的微信公众号,每天学习Go知识
以上所述就是小编给大家介绍的《Go 每日一库之 zap》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
我看电商3:零售的变革
黄若 / 电子工业出版社 / 2018-4 / 49
在《我看电商3:零售的变革》之前,黄若先生的“我看电商”系列图书《我看电商》《再看电商》《我看电商2》,均为行业畅销书。黄若先生的图书有两大特如一是干货满满,二是观点鲜明。 “新零售”是眼下的热门词。在2017年里,数以万计的企业以“新零售”作为标识进入市场。但是社会上对“新零售“存在着各种模糊的定义和不尽相同的解读。 《我看电商3:零售的变革》中明确提出:新零售不应过分关注于渠道形式......一起来看看 《我看电商3:零售的变革》 这本书的介绍吧!