Go日志库-Uber开源库zap使用

CKeengolanggolang的常用库约 2010 字大约 7 分钟

作者:程序员CKeen
博客:http://ckeen.cnopen in new window

长期坚持做有价值的事!积累沉淀,持续成长,升维思考!希望把编码作为长期兴趣爱好😄


之前我们讲到go日志官方标准库的log的使用方法介绍,官方log标准库虽然简单,也不依赖第三方实现,但是功能相对较弱,不支持日志级别open in new window、日志分割、日志格式等,所以在项目中很少直接使用,通用于快速调试和验证。这一篇我们来介绍一下Uber官方开源日志库zap。

1.zap包介绍

zapopen in new window是uber开源的日志包,以高性能著称。zap除了具有日志基本的功能之外,还具有很多强大的特性:

  • 支持常用的日志级别,例如:Debug、Info、Warn、Error、DPanic、Panic、Fatal。
  • 性能非常高,适合对性能要求比较高的场景。
  • 支持结构化的日志记录。
  • 支持预设日志字段。
  • 支持针对特定的日志级别,输出调用堆栈。
  • 能够打印基本信息,如调用文件/函数名和行号,日志时间等。
  • 支持hook。

zap对性能的优化的点:

  • 使用强类型,而避免使用interface{}, zap提供zap.String, zap.Int等基础类型的字段输出方法。
  • 不使用反射。反射是有代价的,用户在记录的日志的时候,应该清楚每个字段填充需要传入的类型。
  • 输出json格式化数据时,不使用json.Marshal和fmt.Fprintf基于interface{}反射的实现方式,而是自己实现了json Encoder, 通过明确的类型调用,直接拼接字符串,最小化性能开销。
  • 使用sync.Pool缓存来复用对象,降低了GC。zap中zapcore.Entry和zap.Buffer都是用了缓存池技术,zapcorea.Entry代表一条完整的日志消息。

官方对zap和其他日志库性能还做了对比,如下图:

4fd0b6a6529043a5b4e8875a9c04ff8e.png点击并拖拽以移动

8f632a07ee8a4e2eac35f8aaabd2cec0.png点击并拖拽以移动

2. zap的使用

首先我们安装zap库

> go get -u go.uber.org/zap

Zap提供了两种类型的日志记录:SugaredLoggerLogger

在性能很好但不是关键的上下文中,可以使用SugaredLogger。它比其他结构化日志包快4-10倍,并且包含结构化和printf风格的api。

当性能和类型安全性非常关键时,请使用Logger。它比SugaredLogger更快,占用内存也少得多,但它只支持结构化日志。

下面我们写一个简单的实例来看下zap的logger的使用

logger := zap.NewExample()
defer logger.Sync()

const url = "http://api.ckeen.cn"

logger.Info("fetch url:",zap.String("url", url))

sugar := logger.Sugar()

sugar.Infow("Failed to fetch URL.",
    "url", url,
    "attempt", 3,
    "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

logger.Info("Failed to fetch URL.",
    zap.String("url", url),
    zap.Int("attempt", 3),
    zap.Duration("backoff", time.Second),
)

// 打印输出结果
// {"level":"info","ts":1655827513.5442681,"caller":"zap/main.go:22","msg":"fetch url:","url":"http://api.ckeen.cn"}
// {"level":"info","ts":1655827513.544397,"caller":"zap/main.go:25","msg":"Failed to fetch URL.","url":"http://api.ckeen.cn","attempt":3}
// {"level":"info","ts":1655827513.54445,"caller":"zap/main.go:30","msg":"Failed to fetch URL: http://api.ckeen.cn"}
// {"level":"info","ts":1655827513.5444632,"caller":"zap/main.go:32","msg":"Failed to fetch URL.","url":"http://api.ckeen.cn","attempt":3}

2.1 创建Logger实例

  • zap提供了NewExample/NewProduction/NewDevelopment构造函数创建Logger

这里我们使用的zap.NewExample()创建一个Logger的实例。我看官方的API接口提供了的函数:

4ac8a0b7ac574dc7b79bd8cbbfb82fee.png点击并拖拽以移动

  • 使用Logger对象返回SugaredLogger

除了创建Logger,我们还可以根据创建的Logger对象的Sugar()函数返回SugaredLogger的实例。SugaredLogger的结构化日志记录api是松散类型的。

就Info函数来说,除了可以输出第一个参数是string类型的msg外,还可以输出多个Field类型的参数。而Field类型是由zap包下的不同数据类型的函数来构造,比如上述实例代码中,我们使用zap.String()来构造了一个Field。查看zap.String的源码可以看到它构造Filed的类型数据:

025aa484956945cfb967a94aa209c8ed.png点击并拖拽以移动

zap包下还提供了其他zap.Int,zap.Bool,zap.Float32等函数用来支持不同的数据类型。具体的函数可以参考文末的资料。

3.使用Namespace构建嵌套格式

我们可以使用zap.Namespace(key string) Field构建一个命名空间,后续的Field都记录在此命名空间中,这样就可以形成一个json的嵌套输出:

func main(){
	logger := zap.NewExample()
	defer logger.Sync()

	logger.Info("top level log",
		zap.Namespace("params"),
		zap.String("desc", "sub level log"),
		zap.Int("traceId", 1),
		zap.Int("spanId", 1),
		)

	logger2 := logger.With(
		zap.Namespace("params"),
		zap.String("desc", "sub level log"),
		zap.Int("traceId", 1),
		zap.Int("spanId", 1),
	)
	logger2.Info("tracked sub level")
}

// 打印结果
// {"level":"info","msg":"top level log","params":{"desc":"sub level log","traceId":1,"spanId":1}}
// {"level":"info","msg":"tracked sub level","params":{"desc":"sub level log","traceId":1,"spanId":1}}

从示例和打印结果可以看到,可以使用zap的Info的Filed参数传入zap.Namespace的方式输出嵌套结构,也可以是用With函数构建一个嵌套结构的新的Logger再进行输出。这里要注意是Filed参数是有顺序的,排在zap.Namespace后面的Field字段才会嵌套在namespace的的结构里面

4.构造的Logger对象的选项Option

我们可以看到上述构造Logger对象提供的NewExample/NewProduction/NewDevelopment的函数都提供了Option类型的参数列表来创建Logger对象,官方给我们提供了以下的Option参数:

ead7037cf2d043a8a6aad9a1fb670828.png点击并拖拽以移动

下面主要通过几个重点的函数,来看下zap提供功能:

在创建Logger对象的时候用,使用AddCaller和WithCaller能答应调用的文件的名和行号。

不过在上述的NewExample/NewProduction/NewDevelopment中,使用NewProduction/NewDevelopment创建Logger实例,传入AddCaller和WithCaller的Option是有效的,而NewExampleAddCaller和WithCaller传入Option是无效的。

查看了一下源码是跟创建Logger的Config的时候,传入的EncoderConfig的参数有关,当创建Encoder的时候时候如果设置CallerKey,才能输出caller信息,而NewExample创建Logger的时候没有设置该参数。参考下面代码的打印输出结果:

  logger := zap.NewExample(zap.AddCaller())
	defer logger.Sync()
	logger.Info("tracked root level", zap.String("foo", "bar"))

	logger2, _ := zap.NewProduction(zap.AddCaller())
	defer logger2.Sync()

	logger2.Info("tracked root level", zap.String("foo", "bar"))

	logger3, _ := zap.NewDevelopment(zap.AddCaller())
	defer logger3.Sync()

	logger3.Info("tracked root level", zap.String("foo", "bar"))

打印输出结果:

{"level":"info","msg":"tracked NewExample root level","foo":"bar"} {"level":"info","ts":1655890681.2474399,"caller":"zap/main.go:99","msg":"tracked NewProduction root level","foo":"bar"} 2022-06-22T17:38:01.247+0800 INFO zap/main.go:105 tracked NewDevelopment root level

4.2 AddStacktrace输出调用堆栈

Logger的AddStacktrace可以控制打印不同的级别的代码调用的详细的堆栈信息,详细实例可以参考下方Fields的设置部分代码

4.3 Fields设置预设日志字段

通过Fileds的Option设置,我们可以配置一些日志包含的通用字段,例如设置日志的traceId等,示例如下:

logger,_ := zap.NewProduction(zap.Fields(
	zap.Namespace("sample4"),
	zap.String("traceId", "1100"),
	zap.String("spanId", "200")),             // 设置预设字段
	zap.AddStacktrace(zapcore.DebugLevel),    //设置输出调用堆栈信息
	zap.WithCaller(true))
defer logger.Sync()

logger.Info("print one log", zap.String("foo", "bar"))

打印结果:

{"level":"info","ts":1655890937.63424,"caller":"zap/main.go:116","msg":"print one log","sample4":{"method":"main","foo":"bar"},"stacktrace":"main.sample4\n\t/Users/ckeen/Documents/code/gosource/go-awesome/go-samples/zap/main.go:116\nmain.main\n\t/Users/ckeen/Documents/code/gosource/go-awesome/go-samples/zap/main.go:128\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:255"}

4.4 Hooks为日志提供钩子函数

Hooksw为Logger的提供了注册的钩子函数的选项。根据下面Hooks的函数定义,可以看到当Logger每次写出一个Entry时,这些函数将被调用。

func Hooks(hooks ...func(zapcore.Entry) error) Option

我们可以通过注册的钩子函数来做一些日志的统计工作或者转发工作,比如转发到kafka之类的。

参考资料

  1. https://pkg.go.dev/go.uber.org/zapopen in new window
  2. https://medium.com/a-journey-with-go/go-how-zap-package-is-optimized-dbf72ef48f2dopen in new window
  3. 深度 | 从Go高性能日志库zap看如何实现高性能Go组件open in new window