Go 每日一库之 zap
转载地址:Go 每日一库之 zap - SegmentFault 思否
简介
在很早之前的文章中,我们介绍过 Go 标准日志库log和结构化的日志库logrus。在热点函数中记录日志对日志库的执行性能有较高的要求,不能影响正常逻辑的执行时间。uber
开源的日志库zap
,对性能和内存分配做了极致的优化。
快速使用
先安装:
$ go get go.uber.org/zap
后使用:
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) }
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.Configif 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:13main.f1d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9main.maind:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22runtime.mainC:/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
Go 每日一库之 zap相关推荐
- 每日一库之Go 强大而灵活的电子邮件库:email
发送邮件是一个很常见的需求:用户邮箱验证.邮箱召回等.Go 语言标准库自带 net/smtp 库,实现了 smtp 协议,用于发送邮件.然而这个库比较原始,使用不方便,而且官方声明不再增加新功能.于是 ...
- go get 失败 no go files in_Go 每日一库之 dig
简介 今天我们来介绍 Go 语言的一个依赖注入(DI)库--dig.dig 是 uber 开源的库.Java 依赖注入的库有很多,相信即使不是做 Java 开发的童鞋也听过大名鼎鼎的 Spring.相 ...
- go float64 比较_Go 每日一库之 plot
Go 每日一库之 plot 简介 本文介绍 Go 语言的一个非常强大.好用的绘图库--plot.plot内置了很多常用的组件,基本满足日常需求.同时,它也提供了定制化的接口,可以实现我们的个性化需求. ...
- go get 的不再src目录中_Go 每日一库之 sqlc:根据 sql 生成代码
简介 在 Go 语言中编写数据库操作代码真的非常痛苦!database/sql标准库提供的都是比较底层的接口.我们需要编写大量重复的代码.大量的模板代码不仅写起来烦,而且还容易出错.有时候字段类型修改 ...
- go 默认http版本_【每日一库】超赞的 Go 语言 INI 文件操作
点击上方蓝色"Go语言中文网"关注我们,领全套Go资料,每天学习 Go 语言 如果你使用 INI 作为系统的配置文件,那么一定会使用这个库吧.没错,它就是号称地表 最强大.最方便 ...
- go get如何删除_Go 每日一库之 xorm
简介 Go 标准库提供的数据库接口database/sql比较底层,使用它来操作数据库非常繁琐,而且容易出错.因而社区开源了不少第三方库,如上一篇文章中的sqlc工具,还有各式各样的 ORM (Obj ...
- go 根据输入类型执行对应的方法_Go 每日一库之 sqlc
简介 在 Go 语言中编写数据库操作代码真的非常痛苦!database/sql标准库提供的都是比较底层的接口.我们需要编写大量重复的代码.大量的模板代码不仅写起来烦,而且还容易出错.有时候字段类型修改 ...
- Go 每日一库之 xorm
简介 Go 标准库提供的数据库接口database/sql比较底层,使用它来操作数据库非常繁琐,而且容易出错.因而社区开源了不少第三方库,如上一篇文章中的sqlc工具,还有各式各样的 ORM (Obj ...
- Go 每日一库之 gorilla/mux
简介 gorilla/mux是 gorilla Web 开发工具包中的路由管理库.gorilla Web 开发包是 Go 语言中辅助开发 Web 服务器的工具包.它包括 Web 服务器开发的各个方面, ...
最新文章
- CSS3常用动画总结
- 【转】HMAC哈希消息认证码及算法原理
- 技术演讲的技巧和经验
- mailbox 编程_MailboxProcessor从C#
- float和position
- csv 读写 python_Python CSV读写
- java 取余 负数_Java中有关负数取余的计算
- 远程桌面计算机密码是多少,局域网远程桌面连接密码
- 高中的班花貌美如花,我与一众兄弟将其“共享”了!
- 脑与认知神经科学Matlab Psytoolbox认知科学实验设计——视错觉
- 水泥cement或英语caement水泥
- 重磅 !微软官方出了免费 Python 视频教程
- 基于PCA的降维中,进行特征值分解和SVD分解相关笔记
- php创始人不建议使用框架,PHP大师指点:优秀的PHP代码怎么来?
- 12、微信前端开发利器:WeUI
- SAP现金管理(Cash Management)的基本概念
- 电路返回端,接地,大地,等势体,静电场,回路,电能
- Linux设置服务自启动
- 杨鹏:17天搞定gre单词的方法
- ieee下载文献的方法
热门文章
- 无法解析 uafxcw.lib_二级建造师《实务科目》推荐知识点习题,附中业网校答案解析...
- 排序 -> 插入排序
- mysql 增量备份_MySQL增量备份与恢复(增量备份概述、特点,断点恢复实操)
- 关于ORACLE数据传输加密的介绍
- 25马5跑道,求最快的五匹马的需要比赛的次数
- C++雾中风景4:多态引出的困惑,对象的拷贝?
- JS异步加载,JQ事件不被执行解决方法
- iOS vuforia 学习钻研(一)
- Android 中的 Service 全面总结(二)
- MooTools1.3.1 API(Core)学习及试译(三)——Types(二)