高性能日志库——Zap
今天把项目中的日志部分完成了,内容还是比较多的,所以从写代码到看各种函数花了十几个小时。那就还是老样子,先总结一下最重要的部分——Zap库的基本使用方法。
日志系统
在任何一个投入使用的项目中,都需要一个好的日志系统(关于什么是日志,可以看之前的总结,更建议看总结中的参考)。在 Go 语言中,我们有很多能够投入使用的日志库,比如log
、zap
等。对于一个好的日志记录器来说,我们需要它能够实现一下功能:
- 能够将事件记录到文件中,而不是应用程序控制台。
- 日志切割-能够根据文件大小、时间或间隔等来切割日志文件。
- 支持不同的日志级别。例如INFO,DEBUG,ERROR等。
- 能够打印基本信息,如调用文件/函数名和行号,日志时间等。
默认的Go Logger
在学习Uber-go的zap包之前,还是先学习一下Go语言提供的基本日志功能。由于之前写项目的时候在日志系统设计方面并没有很在意,所以根本就没记住什么东西,还是重新学一下吧。
如何使用
实现一个Go语言中的日志记录器非常简单——创建一个新的日志文件,然后设置它为日志的输出位置。
那就来看一段简单的实现代码吧:
1 | package main |
当我们执行上述代码后,会有一个 test.log
文件被创建,文件中的内容为:
1 | 2024/05/25 18:03:00 Error fetching url www.baidu.com : Get "www.baidu.com": unsupported protocol scheme "" |
优势与劣势
优势
它最大的优点是使用非常简单。我们可以设置任何io.Writer
作为日志记录输出并向其发送要写入的日志。
劣势
- 仅限基本的日志级别
- 只有一个
Print
选项。不支持INFO
/DEBUG
等多个级别。
- 只有一个
- 对于错误日志,它有
Fatal
和Panic
- Fatal日志通过调用
os.Exit(1)
来结束程序 - Panic日志在写入日志消息之后抛出一个panic
- 但是它缺少一个ERROR日志级别,这个级别可以在不抛出panic或退出程序的情况下记录错误
- Fatal日志通过调用
- 缺乏日志格式化的能力——例如记录调用者的函数名和行号,格式化日期和时间格式。等等。
- 不提供日志切割的能力。
Uber-go Zap
由于上面提到的 Logger
库的劣势并不能帮助我们设计一个良好的日志记录器,所以在本项目中选择了另一个功能更丰富等库——Zap
。它同时提供了结构化日志记录和 printf 风格的日志记录。
那这时候就有同学会有疑问了,提供了更多的功能会不会导致在完成设定的任务时变得更慢呢?
一个优秀的工具就是实现了既要又要,根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好——也比标准库更快。 以下是Zap发布的基准测试信息
记录一条消息和10个字段:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
⚡️ zap | 862 ns/op | +0% | 5 allocs/op |
⚡️ zap (sugared) | 1250 ns/op | +45% | 11 allocs/op |
zerolog | 4021 ns/op | +366% | 76 allocs/op |
go-kit | 4542 ns/op | +427% | 105 allocs/op |
apex/log | 26785 ns/op | +3007% | 115 allocs/op |
logrus | 29501 ns/op | +3322% | 125 allocs/op |
log15 | 29906 ns/op | +3369% | 122 allocs/op |
记录一个静态字符串,没有任何上下文或printf风格的模板:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
⚡️ zap | 118 ns/op | +0% | 0 allocs/op |
⚡️ zap (sugared) | 191 ns/op | +62% | 2 allocs/op |
zerolog | 93 ns/op | -21% | 0 allocs/op |
go-kit | 280 ns/op | +137% | 11 allocs/op |
standard library | 499 ns/op | +323% | 2 allocs/op |
apex/log | 1990 ns/op | +1586% | 10 allocs/op |
logrus | 3129 ns/op | +2552% | 24 allocs/op |
log15 | 3887 ns/op | +3194% | 23 allocs/op |
Zap的特性
- 高性能:zap 对日志输出进行了多项优化以提高它的性能
- 日志分级:有 Debug,Info,Warn,Error,DPanic,Panic,Fatal 等
- 日志记录结构化:日志内容记录是结构化的,比如 json 格式输出
- 自定义格式:用户可以自定义输出的日志格式
- 自定义公共字段:用户可以自定义公共字段,大家输出的日志内容就共同拥有了这些字段
- 调试:可以打印文件名、函数名、行号、日志时间等,便于调试程序
- 自定义调用栈级别:可以根据日志级别输出它的调用栈信息
- Namespace:日志命名空间。定义命名空间后,所有日志内容就在这个命名空间下。命名空间相当于一个文件夹
- 支持 hook 操作
基本使用
1 | package main |
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()
将缓存同步到文件中。
Zap提供了两种类型的日志记录器—Sugared Logger
和Logger
。
Logger
- 通过调用
zap.NewProduction()
/zap.NewDevelopment()
或者zap.Example()
创建一个Logger。 - 上面的每一个函数都将创建一个logger。唯一的区别在于它将记录的信息不同。例如production logger默认记录调用函数信息、日期和时间等。
- 通过Logger调用Info/Error等。
- 默认情况下日志都会打印到应用程序的console界面。
1 | var logger *zap.Logger |
在上面的代码中,我们首先创建了一个Logger,然后使用Info/ Error等Logger方法记录消息。
Sugared Logger
现在让我们使用Sugared Logger来实现相同的功能。
- 大部分的实现基本都相同。
- 惟一的区别是,我们通过调用主logger的
. Sugar()
方法来获取一个SugaredLogger
。 - 然后使用
SugaredLogger
以printf
格式记录语句
下面是修改过后使用SugaredLogger
代替Logger
的代码:
1 | var sugarLogger *zap.SugaredLogger |
为什么会有两种?
由于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
结尾的方法,这种方式不需要先创建字段对象,直接将字段名和值依次放在参数中即可。
记录层级关系
前面记录的日志都是一层结构,没有嵌套的层级。我们可以使用 zap.Namespace(key string) Field
构建一个命名空间,后续的Filed
都记录在此命名空间中:
1 | func main() { |
输出结果:
1 | {"level":"info","msg":"tracked some metrics","metrics":{"counter":1}} |
上面我们演示了两种Namespace
的用法,一种是直接作为字段传入Debug/Info
等方法,一种是调用With()
创建一个新的Logger
,新的Logger
记录日志时总是带上预设的字段。
定制Logger
调用NexExample()/NewDevelopment()/NewProduction()
这 3 个方法,zap
使用默认的配置。我们也可以手动调整,配置结构如下:
1 | // src/go.uber.org/zap/config.go |
Level
:日志级别;Encoding
:输出的日志格式,默认为 JSON;OutputPaths
:可以配置多个输出路径,路径可以是文件路径和stdout
(标准输出);ErrorOutputPaths
:错误输出路径,也可以是多个;InitialFields
:每条日志中都会输出这些值。
其中EncoderConfig
为编码配置:
1 | // src/go.uber.org/zap/zapcore/encoder.go |
MessageKey
:日志中信息的键名,默认为msg
;LevelKey
:日志中级别的键名,默认为level
;EncodeLevel
:日志中级别的格式,默认为小写,如debug/info
。
将日志写入文件而不是终端
在实际运行的项目中,将日志写入终端显然不是一个好的想法,即不方便查看,又不能够长时间存储。所以,将日志单独写入到一个文件夹中,是一个非常好的设计。这时,我们就要用到zap.New()
方法来定制创建logger。
1 | func New(core zapcore.Core, options ...Option) *Logger |
zapcore.Core
需要三个配置——Encoder
,WriteSyncer
,LogLevel
。
- Encoder:编码器(如何写入日志)。
- WriterSyncer :指定日志将写到哪里去。
- Log Level:哪种级别的日志将被写入。
在本项目的实际开发中,对以上三个配置都做出了相应的设计。根据日期以及日志的等级实现了相应的日志分割功能,将日志输出的时间格式也进行了更加简洁的设置。
全局Logger
为了方便使用,zap
提供了两个全局的Logger
,一个是*zap.Logger
,可调用zap.L()
获得;另一个是*zap.SugaredLogger
,可调用zap.S()
获得。需要注意的是,全局的Logger
默认并不会记录日志!它是一个无实际效果的Logger
。看源码:
1 | // go.uber.org/zap/global.go |
我们可以使用ReplaceGlobals(logger *Logger) func()
将logger
设置为全局的Logger
,该函数返回一个无参函数,用于恢复全局Logger
设置:
1 | func main() { |
输出:
1 | {"level":"info","msg":"global Logger after"} |
可以看到在调用ReplaceGlobals
之前记录的日志并没有输出。
总结
以上就是在本项目中已经使用的有关 Zap
库的内容,更加详细和底层的东西还是去看大佬的博客和实际源码吧,我写的博客也只是为了强化一下记忆。
呜呜呜……,暑期实习还是没有一点眉目,不知道该怎么办了,真是不想背八股文啊!!!昨天面了边无际的面试,感觉这才应该是面试该有的情况,就不要问那些要靠死记硬背的八股文了,日常工作又能用到多少,谁会在写代码的时候需要考虑三次握手怎么握呀。不过不问八股好像也没什么可问的了。这就是绝大多数本科生的悲哀之处吧,上了四年学,什么都没学到。
不过边无际的面试面的好像也没有多好,不过这是我三个月以来说过最流畅的自我介绍了,感觉还是得深入的学习一下Redis和Docker的内容了。