zap 是 Uber 开源的 go语言的日志库,它的优势在于实时写结构化日志(Structured Logging)到文件有很好的性能。结构化日志就是说相比于直接输出日志文本,使用 json 或者其它编码方式使日志结构化,这样可以方便后续用各种工具分析处理和查找,比如用 ELK(Elasticsearch, Logstash and Kibana)。根据 zap 自己的基准库测试结果,它比其它结构化日志的库(比如我之前使用的 logrus )要有更好的性能。接下来主要介绍一下 zap 库的使用方法。
注:下文将忽略引用库的代码:
代码语言:javascript复制import "go.uber.org/zap"
全局的 logger
zap 的基础用法是创建一个 logger 实例,然后在所有要用它的地方将它作为参数传过去用:
代码语言:javascript复制logger, _ := zap.NewProduction()
defer logger.Sync() // 将 buffer 中的日志写到文件中
logger.Info("this is a test log")
或者是用一个全局的 logger 实例,zap 库自己提供的全局的 logger 是zap.S()
和 zap.L()
。可以用 ReplaceGlobals
来将全局的 logger 替换为我们通过配置定制的 logger :
logger := zap.NewExample()
defer logger.Sync()
undo := zap.ReplaceGlobals(logger)
defer undo()
zap.L().Info("replaced zap's global loggers")
Sugar 的作用
zap 默认的 logger 不支持格式化输出,要打印指定值要用 zap.String
、zap.Int
等封装,代码就显得非常冗长,如:
logger, _ := zap.NewDevelopment() // 忽略了错误
logger.Info("this is a test log", zap.String("name", "x"), zap.Int("age", 20))
但它提供了 Sugar(语法糖的糖),只要一点点额外的性能损失(但是仍比大部分库快),可以比较简单地格式化输出。
代码语言:javascript复制sugar := logger.Sugar()
sugar.Infof("name is %s", "x") // 格式化输出
sugar.Infow("this is a test log", "name", "x", "age", 20) // 第二个开始每一对是一个键值
// 使用全局的 SugaredLogger
name := "x"
zap.S().Info("this is a test log: name=", name) // 用法相当于 fmt.Print
定制 zap 提供了可配置的 logger,配置一个 logger 时至少需要以下设置:
代码语言:javascript复制logger, _ := zap.Config{
Encoding: "json", // 配置编码方式(json 或 console)
Level: zap.NewAtomicLevelAt(zapcore.DebugLevel), // 输出级别
OutputPaths: []string{"stdout"}, // 输出目的地
}.Build()
其中 OutputPaths 可以用来设置希望日志输出到的文件路径。不过上面这样设置后,message 信息就不能打印出来:
代码语言:javascript复制logger.Info("something to log") // 只会打印 {}
logger.Sugar().Infow("test", "name", "xxx") // 也只打印 {"name": "xxx"}
需要如下设置EncoderConfig
中的 MessageKey
才能打印出message,同时也可设置需要打印的其它key:
logger, _ := zap.Config{
Encoding: "json",
Level: zap.NewAtomicLevelAt(zapcore.DebugLevel),
OutputPaths: []string{"stdout"},
EncoderConfig: zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "level",
EncodeLevel: zapcore.CapitalLevelEncoder,// INFO
TimeKey: "time",
EncodeTime: zapcore.ISO8601TimeEncoder,
CallerKey: "caller",
EncodeCaller: zapcore.ShortCallerEncoder,
},
}.Build()
其中:
- 时间可以设置为 ISO 8601 format,或者 Unix时间戳(秒、毫秒、纳秒)
- level 可以设置为 capital 或者 lowercase,还可设置有颜色的,但是json方式输出时不会生效。
- caller 就是调用者,可以设置 short (package/file:line)或者 full( /full/path/to/package/file:line)。
另外,也可以使用 zap 提供的 ProductionEncoderConfig 等配置,进行定制:
代码语言:javascript复制cfg := zap.NewProductionEncoderConfig()
cfg.EncodeTime = zapcore.ISO8601TimeEncoder
file, _ := os.Create(LogPath)
core := zapcore.NewTee(
zapcore.NewCore(zapcore.NewJSONEncoder(cfg), zapcore.AddSync(file), zapcore.InfoLevel),
zapcore.NewCore(zapcore.NewConsoleEncoder(cfg)), zapcore.Lock(os.Stdout), zapcore.DebugLevel),
)
logger := zap.New(core)
defer logger.Sync()
Sync 的作用
注意到官方文档提示说要在退出程序前执行一下logger.Sync()
, 它是做什么的呢?默认情况,Linux 写文件都是异步的,写的内容会先缓存在内存里,在合适的时间刷(flush)到磁盘中。而 Sync 是一个强制将缓存的数据立刻刷入磁盘的命令。所以 GoLang 标准库中的 File 就有 Sync 函数来对应这个命令。因此 logger.Sync()
做的事情就是对所有输出目标文件执行 Sync。
如果不执行,就有可能导致部分内容没有被写入磁盘。
为什么性能比较好?
最后我们简单看看 zap 为提高性能做的主要工作。zap 提供的默认的 logger 避免了 interface{}
的存在(因为你必须声明 Field 的类型),这样减少了反射(Reflection)用的时间。且 zap 使用了 sync.Pool
来减少给 buffer 分配空间的时间。
总结一下,使用 zap 不需要我们付出多少额外的工作量,却可以得到比较明显的性能提升,因此如果你的项目需要输出结构化的日志到文件,不妨使用 zap。