高性能日志库——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的内容了。
