Zap高性能日志库实践
Zap 是一个由 Uber 公司开源的结构化、高性能日志记录库,旨在为 Go 语言提供一种快速、简单且高效的日志解决方案。它起源于 Uber 内部使用的日志系统,后来于 2016 年开源,迅速获得了 Go 社区的广泛关注和应用。
Zap 的主要特点如下:
- 高性能:Zap 在设计时就非常注重性能,比标准库 log 包快几个数量级,即使在高并发场景下也能保持出色的性能表现。
- 结构化日志:Zap 支持结构化日志记录,可以方便地记录任意类型的字段,而不仅限于字符串,这有利于后期日志分析和处理。
- 级别控制:Zap 提供了丰富的日志级别控制,可以动态修改日志级别,从而只输出关键日志或调试日志。
- 编码支持:Zap 内置支持 JSON 和控制台的日志编码,并提供了钩子机制来扩展其他编码格式。
- 日志分割:Zap 支持根据日期、大小等条件自动分割日志文件,方便日志文件管理和分析。
Zap 广泛应用于各种 Go 项目中,尤其是那些对性能、日志结构化和可观测性有较高要求的场景,如微服务、分布式系统等。很多知名的 Go 项目和公司都在使用 Zap,例如 Kubernetes、Istio、InfluxData 等。通过 Zap,开发者可以获得高效、灵活且易于管理的日志解决方案,从而更好地监控和调试应用程序。
下面我们来进行zap日志库的上手实践。
依赖
我个人比较习惯配置在 go.mod
文件当中,但是搜索了几页居然都没有发现,只好采用了官方给的命令安装依赖方式:
go get -u go.uber.org/zap
然后我发现了 go.mod
文件已经有了相对应的配置,如下:
go.uber.org/zap v1.27.0 // indirect
在后面实践当中还会用到其他的依赖,这里一起发一下配置:
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
小试牛刀
下面我们先来一个基础的Case来熟悉一下zap日志库的的使用语法:
//
// TestLogZap
// @Description: 测试zap日志
// @param t
//
func TestLogZap(t *testing.T) {
logger, _ := zap.NewProduction() // 创建一个新的 Logger 实例
defer logger.Sync() // 确保缓冲区中的日志条目被刷新
logger.Info("FunTester,例子", // 使用 logger 记录日志
zap.String("name", "FunTester"), // 结构化上下文
zap.Int("score", 100), // 结构化上下文
)
logger.Info("warn FunTester coming!!!") // 使用 logger 记录警告日志
logger.Warn("warn FunTester coming!!!") // 使用 logger 记录警告日志
logger.Error("error FunTester coming!!!") // 使用 logger 记录错误日志
}
下面是控制台输出:
=== RUN TestLogZap
{"level":"info","ts":1717310460.23924,"caller":"test/zap_test.go:16","msg":"This is an info message","category":"example","counter":1}
{"level":"info","ts":1717310460.2393,"caller":"test/zap_test.go:20","msg":"warn FunTester coming!!!"}
{"level":"warn","ts":1717310460.2393029,"caller":"test/zap_test.go:21","msg":"warn FunTester coming!!!"}
{"level":"error","ts":1717310460.239305,"caller":"test/zap_test.go:22","msg":"error FunTester coming!!!","stacktrace":"funtester/test.TestLogZap\n\t/Users/oker/GolandProjects/funtester/test/zap_test.go:22\ntesting.tRunner\n\t/opt/homebrew/opt/go/libexec/src/testing/testing.go:1689"}
--- PASS: TestLogZap (0.00s)
PASS
可以看到,这里的输出格式均是 JSON
格式的日志信息,对于不同的级别,输出的日志信息中,都包含了 caller
信息,但是 error
日志多了一个 stacktrace
信息。
这里是我查到的zap默认的配置信息:
Debug 级别日志:包含调用者信息,但不包含堆栈信息。
Info 级别日志:包含调用者信息,但不包含堆栈信息。
Warn 级别日志:包含调用者信息,但不包含堆栈信息。
Error 级别日志:包含调用者信息,并包含堆栈信息。
DPanic 级别日志:包含调用者信息,并包含堆栈信息。
Panic 级别日志:包含调用者信息,并包含堆栈信息。
Fatal 级别日志:包含调用者信息,并包含堆栈信息。
sugar
在 zap 日志库中,除了提供高性能、结构化的日志记录功能外,还提供了一个简化的日志记录接口,称为 “Sugared Logger”。Sugared Logger 提供了一种更简便的方式来记录日志,适合那些不需要严格结构化日志的场景。
Sugared Logger(糖化日志记录器)是一种在使用上更灵活、语法更简洁的日志记录器。与 zap 的原生结构化日志记录器相比,Sugared Logger 提供了类似于 fmt.Printf 风格的方法,这使得记录日志更为简便,但在性能上略有损失。
下面是一个使用的例子:
func TestLogZapSugar(t *testing.T) {
logger, _ := zap.NewProduction() // 创建一个新的 Logger 实例
defer logger.Sync() // 确保缓冲区中的日志条目被刷新
sugar := logger.Sugar() // 使用 Sugar 方法创建一个新的 Logger 实例
sugar.Infow("调用失败", // 使用 Sugar 方法记录日志
"方法", "FunTester",
"调用次数", 3,
"时间单位", time.Second,
)
sugar.Infof("调用方法失败 %s", "FunTester") // 使用 Sugar 方法记录日志
}
下面是日志输出:
=== RUN TestLogLevel
2024-06-02T14:57:28.298+0800 INFO test/zap_test.go:62 This is a custom logger info message {"category": "custom", "counter": 1}
2024-06-02T14:57:28.299+0800 WARN test/zap_test.go:66 This is a custom logger warning message
2024-06-02T14:57:28.299+0800 ERROR test/zap_test.go:67 This is a custom logger error message
2024-06-02T14:57:28.299+0800 INFO test/zap_test.go:68 This is a structured log message {"key1": "value1", "key2": 42}
--- PASS: TestLogLevel (0.00s)
PASS
这样看起来是不是就更加如何常见的日志格式了条例清理,不同的信息按列显示。
日志等级
下面我们来演示一下如何更加精细化使用日志等级,将超过某个等级的日志输出到控制台上。代码如下:
func TestLogLevel(t *testing.T) {
encoderConfig := zapcore.EncoderConfig{ // 创建编码配置
TimeKey: "T", // 时间键
LevelKey: "L", // 日志级别键
NameKey: "log", // 日志名称键
CallerKey: "C", // 日志调用键
MessageKey: "msg", // 日志消息键
StacktraceKey: "stacktrace", // 堆栈跟踪键
LineEnding: zapcore.DefaultLineEnding, // 行结束符,默认为 \n EncodeLevel: zapcore.CapitalLevelEncoder, // 日志级别编码器,将日志级别转换为大写
EncodeTime: zapcore.ISO8601TimeEncoder, // 时间编码器,将时间格式化为 ISO8601 格式
EncodeDuration: zapcore.StringDurationEncoder, // 持续时间编码器,将持续时间编码为字符串
EncodeCaller: zapcore.ShortCallerEncoder, // 调用编码器,显示文件名和行号
}
encoder := zapcore.NewConsoleEncoder(encoderConfig) // 创建控制台编码器,使用编码配置
atomicLevel := zap.NewAtomicLevel() // 创建原子级别,用于动态设置日志级别
atomicLevel.SetLevel(zap.InfoLevel) // 设置日志级别,只有 Info 级别及以上的日志才会输出
core := zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), atomicLevel) // 将日志输出到标准输出
logger := zap.New(core, zap.AddCaller(), zap.Development()) // 创建 Logger,添加调用者和开发模式
defer logger.Sync()
logger.Warn("打印警告日志")
logger.Error("打印错误日志")
logger.Info("打印结构化日志",
zap.String("key1", "FunTester"),
zap.Int("key2", 22),
)
}
控制台输出如下:
=== RUN TestLogLevel
2024-06-02T15:29:40.686+0800 WARN test/zap_test.go:61 打印警告日志
2024-06-02T15:29:40.687+0800 ERROR test/zap_test.go:62 打印错误日志
2024-06-02T15:29:40.687+0800 INFO test/zap_test.go:63 打印结构化日志 {"key1": "FunTester", "key2": 22}
--- PASS: TestLogLevel (0.00s)
PASS
可以看到,info以上的日志输出到控制台了。
日志文件
之前我们案例中都没有设置将日志输出到文件,下面我们来学习将日志输入到日志文件中的应用。
func TestLogFile(t *testing.T) {
logDir := "logs" // 日志目录,不存在则创建
if err := os.MkdirAll(logDir, 0755); err != nil { // 创建日志目录
panic(err)
}
logFile := filepath.Join(logDir, "app.log") // 日志文件,不存在则创建
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // 创建日志文件
if err != nil {
panic(err)
}
encoderConfig := zapcore.EncoderConfig{ // 创建编码配置
TimeKey: "T", // 时间键
LevelKey: "L", // 日志级别键
NameKey: "log", // 日志名称键
CallerKey: "C", // 日志调用键
MessageKey: "msg", // 日志消息键
StacktraceKey: "stacktrace", // 堆栈跟踪键
LineEnding: zapcore.DefaultLineEnding, // 行结束符,默认为 \n EncodeLevel: zapcore.CapitalLevelEncoder, // 日志级别编码器,将日志级别转换为大写
EncodeTime: zapcore.ISO8601TimeEncoder, // 时间编码器,将时间格式化为 ISO8601 格式
EncodeDuration: zapcore.StringDurationEncoder, // 持续时间编码器,将持续时间编码为字符串
EncodeCaller: zapcore.ShortCallerEncoder, // 调用编码器,显示文件名和行号
}
encoder := zapcore.NewJSONEncoder(encoderConfig) // 创建 JSON 编码器
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig) // 创建控制台编码器
writeSyncer := zapcore.AddSync(file) // 创建 WriteSyncer consoleWriteSyncer := zapcore.AddSync(os.Stdout) // 创建控制台 WriteSyncer atomicLevel := zap.NewAtomicLevel() // 创建原子级别
atomicLevel.SetLevel(zap.InfoLevel) // 设置日志级别
core := zapcore.NewCore(encoder, writeSyncer, atomicLevel) // 创建 Core,将日志输出到文件
consoleCore := zapcore.NewCore(consoleEncoder, consoleWriteSyncer, atomicLevel)
combinedCore := zapcore.NewTee(core, consoleCore) // 创建多个 Core,将日志同时输出到文件和控制台
logger := zap.New(combinedCore, zap.AddCaller(), zap.Development()) // 创建 Logger,添加调用者和开发模式
defer logger.Sync() // 确保缓冲区中的日志条目被刷新
logger.Warn("打印警告日志")
logger.Error("打印错误日志")
logger.Info("打印结构化日志",
zap.String("key1", "FunTester"),
zap.Int("key2", 22),
)
}
控制台输出:
=== RUN TestLogFile
2024-06-02T15:40:30.260+0800 WARN test/zap_test.go:103 打印警告日志
2024-06-02T15:40:30.261+0800 ERROR test/zap_test.go:104 打印错误日志
2024-06-02T15:40:30.261+0800 INFO test/zap_test.go:105 打印结构化日志 {"key1": "FunTester", "key2": 22}
--- PASS: TestLogFile (0.01s)
PASS
日志文件内容:
{"L":"WARN","T":"2024-06-02T15:40:30.260+0800","C":"test/zap_test.go:103","msg":"打印警告日志"}
{"L":"ERROR","T":"2024-06-02T15:40:30.261+0800","C":"test/zap_test.go:104","msg":"打印错误日志"}
{"L":"INFO","T":"2024-06-02T15:40:30.261+0800","C":"test/zap_test.go:105","msg":"打印结构化日志","key1":"FunTester","key2":22}
日志分割
在实际的项目当中,我们通常会对日志进行分割(比如按大小分割),下面我们来演示一下使用zap框架时,进行日志分割的例子。
func TestLogFileLumberjack(t *testing.T) {
writeSyncer := zapcore.AddSync(&lumberjack.Logger{ // 创建 WriteSyncer,使用 lumberjack.Logger,支持日志切割
Filename: "logs/app.log",
MaxSize: 10, // 每个日志文件最大 10 MB MaxBackups: 5, // 保留最近的 5 个日志文件
MaxAge: 30, // 保留最近 30 天的日志
Compress: true, // 旧日志文件压缩
})
encoderConfig := zapcore.EncoderConfig{ // 创建编码配置
TimeKey: "T", // 时间键
LevelKey: "L", // 日志级别键
NameKey: "log", // 日志名称键
CallerKey: "C", // 日志调用键
MessageKey: "msg", // 日志消息键
StacktraceKey: "stacktrace", // 堆栈跟踪键
LineEnding: zapcore.DefaultLineEnding, // 行结束符,默认为 \n EncodeLevel: zapcore.CapitalLevelEncoder, // 日志级别编码器,将日志级别转换为大写
EncodeTime: zapcore.ISO8601TimeEncoder, // 时间编码器,将时间格式化为 ISO8601 格式
EncodeDuration: zapcore.StringDurationEncoder, // 持续时间编码器,将持续时间编码为字符串
EncodeCaller: zapcore.ShortCallerEncoder, // 调用编码器,显示文件名和行号
}
encoder := zapcore.NewJSONEncoder(encoderConfig) // 创建 JSON 编码器
atomicLevel := zap.NewAtomicLevel() // 创建原子级别
atomicLevel.SetLevel(zap.InfoLevel) // 设置日志级别
core := zapcore.NewCore(encoder, writeSyncer, atomicLevel) // 创建 Core,将日志输出到文件
logger := zap.New(core, zap.AddCaller(), zap.Development()) // 创建 Logger,添加调用者和开发模式
defer logger.Sync() // 确保缓冲区中的日志条目被刷新
logger.Warn("打印警告日志")
logger.Error("打印错误日志")
logger.Info("打印结构化日志",
zap.String("key1", "FunTester"),
zap.Int("key2", 22),
)
}
控制台日志打印和文件分割效果这里就不展示了。各位有兴趣可以自测一波。