Golang日志操作库zap的使用详解
作者:九卷
一、简介
zap 是 uber 开源的一个高性能,结构化,分级记录的日志记录包。
go1.20.2
zap v1.24.0
zap的特性
- 高性能:zap 对日志输出进行了多项优化以提高它的性能
- 日志分级:有 Debug,Info,Warn,Error,DPanic,Panic,Fatal 等
- 日志记录结构化:日志内容记录是结构化的,比如 json 格式输出
- 自定义格式:用户可以自定义输出的日志格式
- 自定义公共字段:用户可以自定义公共字段,大家输出的日志内容就共同拥有了这些字段
- 调试:可以打印文件名、函数名、行号、日志时间等,便于调试程序
- 自定义调用栈级别:可以根据日志级别输出它的调用栈信息
- Namespace:日志命名空间。定义命名空间后,所有日志内容就在这个命名空间下。命名空间相当于一个文件夹
- 支持 hook 操作
高性能介绍
与其它日志库对比
看github官网的对比图,下面的对比图来自:https://github.com/uber-go/zap#performance
Log a message and 10 fields:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
⚡ zap | 2900 ns/op | +0% | 5 allocs/op |
⚡ zap (sugared) | 3475 ns/op | +20% | 10 allocs/op |
zerolog | 10639 ns/op | +267% | 32 allocs/op |
go-kit | 14434 ns/op | +398% | 59 allocs/op |
logrus | 17104 ns/op | +490% | 81 allocs/op |
apex/log | 32424 ns/op | +1018% | 66 allocs/op |
log15 | 33579 ns/op | +1058% | 76 allocs/op |
Log a message with a logger that already has 10 fields of context:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
⚡ zap | 373 ns/op | +0% | 0 allocs/op |
⚡ zap (sugared) | 452 ns/op | +21% | 1 allocs/op |
zerolog | 288 ns/op | -23% | 0 allocs/op |
go-kit | 11785 ns/op | +3060% | 58 allocs/op |
logrus | 19629 ns/op | +5162% | 70 allocs/op |
log15 | 21866 ns/op | +5762% | 72 allocs/op |
apex/log | 30890 ns/op | +8182% | 55 allocs/op |
Log a static string, without any context or printf
-style templating:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
⚡ zap | 381 ns/op | +0% | 0 allocs/op |
⚡ zap (sugared) | 410 ns/op | +8% | 1 allocs/op |
zerolog | 369 ns/op | -3% | 0 allocs/op |
standard library | 385 ns/op | +1% | 2 allocs/op |
go-kit | 606 ns/op | +59% | 11 allocs/op |
logrus | 1730 ns/op | +354% | 25 allocs/op |
apex/log | 1998 ns/op | +424% | 7 allocs/op |
log15 | 4546 ns/op | +1093% | 22 allocs/op |
做了哪些优化
基于反射的序列化和字符串格式化,它们都是 CPU 密集型计算且分配很多小的内存。具体到 Go 语言中,使用 encoding/json 和 fmt.Fprintf 格式化 interface{} 会使程序性能降低。
Zap 咋解决呢?Zap 使用一个无反射、零分配的 JOSN 编码器,基础 Logger 尽可能避免序列化开销和内存分配开销。在此基础上,zap 还构建了更高级的 SuggaredLogger。
二、quickstart快速开始
zap 安装:
go get -u go.uber.org/zap
zap 提供了 2 种日志记录器:SugaredLogger
和 Logger
。
在需要性能但不是很重要的情况下,使用 SugaredLogger 较合适。它比其它结构化日志包快 4-10 倍,包括 结构化日志和 printf 风格的 API。看下面使用 SugaredLogger 例子:
logger, _ := zap.NewProduction() defer logger.Sync() // zap底层有缓冲。在任何情况下执行 defer logger.Sync() 是一个很好的习惯 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。它比 SugaredLogger 更快,分配的资源更少,但它只支持结构化日志和强类型字段。
logger, _ := zap.NewProduction() defer logger.Sync() logger.Info("failed to fetch URL", // 字段是强类型,不是松散类型 zap.String("url", url), zap.Int("attempt", 3), zap.Duration("backoff", time.Second), )
三、NewExample/NewDevelopment/NewProduction使用
zap 为我们提供了三种快速创建 logger 的方法: zap.NewProduction()
,zap.NewDevelopment()
,zap.NewExample()
。
见名思义,Example 一般用在测试代码中,Development 用在开发环境中,Production 用在生成环境中。这三种方法都预先设置好了配置信息。
NewExample()使用
NewExample 构建一个 logger,专门为在 zap 的测试示例使用。它将 DebugLevel 及以上日志用 JSON 格式标准输出,但它省略了时间戳和调用函数,以保持示例输出的简短和确定性。
为什么说 zap.NewExample()
是 zap 为我们提供快速创建 logger 的方法呢?
因为在这个方法里,zap 已经定义好了日志配置项部分默认值。来看它的代码:
// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#L127 func NewExample(options ...Option) *Logger { encoderCfg := zapcore.EncoderConfig{ MessageKey: "msg", // 日志内容key:val, 前面的key设为msg LevelKey: "level", // 日志级别的key设为level NameKey: "logger", // 日志名 EncodeLevel: zapcore.LowercaseLevelEncoder, //日志级别,默认小写 EncodeTime: zapcore.ISO8601TimeEncoder, // 日志时间 EncodeDuration: zapcore.StringDurationEncoder, } core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), os.Stdout, DebugLevel) return New(core).WithOptions(options...) }
使用例子:
package main import ( "go.uber.org/zap" ) func main() { logger := zap.NewExample() logger.Debug("this is debug message") logger.Info("this is info message") logger.Info("this is info message with fileds", zap.Int("age", 37), zap.String("agender", "man"), ) logger.Warn("this is warn message") logger.Error("this is error message") }
输出:
{"level":"debug","msg":"this is debug message"} {"level":"info","msg":"this is info message"} {"level":"info","msg":"this is info message with fileds","age":37,"agender":"man"} {"level":"warn","msg":"this is warn message"} {"level":"error","msg":"this is error message"}
NewDevelopment()使用
NewDevelopment() 构建一个开发使用的 Logger,它以人性化的格式将 DebugLevel 及以上日志信息输出。它的底层使用
NewDevelopmentConfig().Build(...Option)
构建。它的日志格式各种设置在函数 NewDevelopmentEncoderConfig() 里,想查看详情设置,请点进去查看。
使用例子:
package main import ( "time" "go.uber.org/zap" ) func main() { logger, _ := zap.NewDevelopment() defer logger.Sync() logger.Info("failed to fetch url", // 强类型字段 zap.String("url", "http://example.com"), zap.Int("attempt", 3), zap.Duration("duration", time.Second), ) logger.With( // 强类型字段 zap.String("url", "http://development.com"), zap.Int("attempt", 4), zap.Duration("duration", time.Second*5), ).Info("[With] failed to fetch url") }
输出:
2023-03-22T16:02:45.760+0800 INFO zapdemos/newdevelopment1.go:13 failed to fetch url {"url": "http://example.com", "attempt": 3, "duration": "1s"} 2023-03-22T16:02:45.786+0800 INFO zapdemos/newdevelopment1.go:25 [With] failed to fetch url {"url": "http://development.com", "attempt": 4, "duration": "5s"}
上面日志输出了文件名和行号,NewExample() 没有
NewProduction()使用
NewProduction() 构建了一个合理的 Prouction 日志记录器,它将 info 及以上的日志内容以 JSON 格式记写入标准错误里。
它的底层使用 NewProductionConfig().Build(...Option)
构建。它的日志格式设置在函数 NewProductionEncoderConfig 里。
使用例子:
package main import ( "time" "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction() defer logger.Sync() url := "http://zap.uber.io" sugar := logger.Sugar() sugar.Infow("failed to fetch URL", "url", url, "attempt", 3, "time", time.Second, ) sugar.Infof("Failed to fetch URL: %s", url) // 或更简洁 Sugar() 使用 // sugar := zap.NewProduction().Sugar() // defer sugar.Sync() }
输出:
{"level":"info","ts":1679472893.2944522,"caller":"zapdemos/newproduction1.go:16","msg":"failed to fetch URL","url":"http://zap.uber.io","attempt":3,"time":1} {"level":"info","ts":1679472893.294975,"caller":"zapdemos/newproduction1.go:22","msg":"Failed to fetch URL: http://zap.uber.io"}
上面日志输出了文件名和行号,NewExample() 没有
使用配置
在这 3 个函数中,可以传入一些配置项。为什么能传入配置项?我们来看看 NewExample() 函数定义:
func NewExample(options ...Option) *Logger
它的函数传参有一个 ...Option
选项,是一个 interface 类型,它关联的是 Logger struct。只要返回 Option 就可以传进 NewExample() 里。在 zap/options.go 文件中可以看到很多返回 Option 的函数,也就是说这些函数都可以传入 NewExample 函数里。这里用到了 Go 里面的一个编码技巧,函数选项模式。
zap.Fields() 添加字段到 Logger 中:
package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction(zap.Fields( zap.String("log_name", "testlog"), zap.String("log_author", "prometheus"), )) defer logger.Sync() logger.Info("test fields output") logger.Warn("warn info") }
输出:
{"level":"info","ts":1679477929.842166,"caller":"zapdemos/fields.go:14","msg":"test fields output","log_name":"testlog","log_author":"prometheus"} {"level":"warn","ts":1679477929.842166,"caller":"zapdemos/fields.go:16","msg":"warn info","log_name":"testlog","log_author":"prometheus"}
zap.Hook() 添加回调函数:
Hook (钩子函数)回调函数为用户提供一种简单方法,在每次日志内容记录后运行这个回调函数,执行用户需要的操作。也就是说记录完日志后你还想做其它事情就可以调用这个函数。
package main import ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { logger := zap.NewExample(zap.Hooks(func(entry zapcore.Entry) error { fmt.Println("[zap.Hooks]test Hooks") return nil })) defer logger.Sync() logger.Info("test output") logger.Warn("warn info") }
输出:
{"level":"info","msg":"test output"} [zap.Hooks]test Hooks {"level":"warn","msg":"warn info"} [zap.Hooks]test Hooks
四、logger和sugaredlogger区别
从上面例子中看出,zap 有 2 种格式化日志方式:logger 和 sugared logger。
sugared logger:
- 它有很好的性能,比一般日志包快 4-10 倍。
- 支持结构化的日志。
- 支持 printf 风格的日志。
- 日志字段不需要定义类型
logger(没有sugar)
- 它的性能比 sugared logger 还要快。
- 它只支持强类型的结构化日志。
- 它应用在对性能更加敏感日志记录中,它的内存分配次数更少。
- 比如如果每一次内存分配都很重要的话可以使用这个。对类型安全有严格要求也可以使用这个。
logger 和 sugaredlogger 相互转换:
// 创建 logger logger := zap.NewExample() defer logger.Sync() // 转换 SugaredLogger sugar := logger.Sugar() // 转换 logger plain := sugar.Desugar()
怎么快速构建一个 logger 呢?有下面种几种方法:
- zap.NewProduction()
- zap.NewDevelopment()
- zap.Example()
主要区别:
- 记录日志信息和结构不同。
- Example 和 Production 是 json 格式输出,Development 是普通一行格式输出,如果后面带有字段输出话用json格式。
相同点:
- 默认情况下都会打印日志信息到 console 界面
- 都是通过 logger 调用 Info、Error 等方法
怎么选择:
- 需要不错的性能但不是很重要的情况下,可以选择 sugaredlogger。它支持结构化日志和 printf 风格的日志记录。sugaredlogger 的日志记录是松散类型的,不是强类型,能接受可变数量的键值对。如果你要用强类型字段记录,可以使用 SugaredLogger.With 方法。
- 如果是每次或每微秒记录日志都很重要情况下,可以使用 logger,它比 sugaredlogger 每次分配内存更少,性能更高。但它仅支持强类型的结构化日志记录。
五、自定义配置
快速构建 logger 日志记录器最简单的方法就是用 zap 预定义了配置的方法:NewExample(), NewProduction()
和NewDevelopment()
,这 3 个方法通过单个函数调用就可以构建一个日志计记录器,也可以简单配置。
但是有的项目需要更多的定制,怎么办?zap 的 Config 结构和 zapcore 的 EncoderConfig 结构可以帮助你,让你能够进行自定义配置。
配置结构说明
Config 配置项源码:
// zap v1.24.0 type Config struct { // 动态改变日志级别,在运行时你可以安全改变日志级别 Level AtomicLevel `json:"level" yaml:"level"` // 将日志记录器设置为开发模式,在 WarnLevel 及以上级别日志会包含堆栈跟踪信息 Development bool `json:"development" yaml:"development"` // 在日志中停止调用函数所在文件名、行数 DisableCaller bool `json:"disableCaller" yaml:"disableCaller"` // 完全禁止自动堆栈跟踪。默认情况下,在 development 中,warnlevel及以上日志级别会自动捕获堆栈跟踪信息 // 在 production 中,ErrorLevel 及以上也会自动捕获堆栈信息 DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"` // 设置采样策略。没有 SamplingConfing 将禁止采样 Sampling *SamplingConfig `json:"sampling" yaml:"sampling"` // 设置日志编码。可以设置为 console 和 json。也可以通过 RegisterEncoder 设置第三方编码格式 Encoding string `json:"encoding" yaml:"encoding"` // 为encoder编码器设置选项。详细设置信息在 zapcore.zapcore.EncoderConfig EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"` // 日志输出地址可以是一个 URLs 地址或文件路径,可以设置多个 OutputPaths []string `json:"outputPaths" yaml:"outputPaths"` // 错误日志输出地址。默认输出标准错误信息 ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"` // 可以添加自定义的字段信息到 root logger 中。也就是每条日志都会携带这些字段信息,公共字段 InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"` }
EncoderConfig 结构源码,它里面也有很多配置选项,具体请看 这里:
// zapcore@v1.24.0 type EncoderConfig struct { // 为log entry设置key。如果 key 为空,那么在日志中的这部分信息也会省略 MessageKey string `json:"messageKey" yaml:"messageKey"`//日志信息的健名,默认为msg LevelKey string `json:"levelKey" yaml:"levelKey"`//日志级别的健名,默认为level TimeKey string `json:"timeKey" yaml:"timeKey"`//记录日志时间的健名,默认为time NameKey string `json:"nameKey" yaml:"nameKey"` CallerKey string `json:"callerKey" yaml:"callerKey"` FunctionKey string `json:"functionKey" yaml:"functionKey"` StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"` SkipLineEnding bool `json:"skipLineEnding" yaml:"skipLineEnding"` LineEnding string `json:"lineEnding" yaml:"lineEnding"` // 日志编码的一些设置项 EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"` EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` // 与其它编码器不同, 这个编码器可选 EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"` // 配置 interface{} 类型编码器。如果没设置,将用 json.Encoder 进行编码 NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"` // 配置 console 中字段分隔符。默认使用 tab ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"` } type Entry struct { Level Level Time time.Time LoggerName string Message string Caller EntryCaller Stack string }
例子1:基本配置
zap.Config 自定义配置,看官方的一个基本例子:
package main import ( "encoding/json" "go.uber.org/zap" ) // https://pkg.go.dev/go.uber.org/zap@v1.24.0#hdr-Configuring_Zap func main() { // 表示 zap.Config 的 json 原始编码 // outputPath: 设置日志输出路径,日志内容输出到标准输出和文件 logs.log // errorOutputPaths:设置错误日志输出路径 rawJSON := []byte(`{ "level": "debug", "encoding": "json", "outputPaths": ["stdout", "./logs.log"], "errorOutputPaths": ["stderr"], "initialFields": {"foo": "bar"}, "encoderConfig": { "messageKey": "message-customer", "levelKey": "level", "levelEncoder": "lowercase" } }`) // 把 json 格式数据解析到 zap.Config struct var cfg zap.Config if err := json.Unmarshal(rawJSON, &cfg); err != nil { panic(err) } // cfg.Build() 为配置对象创建一个 Logger // zap.Must() 封装了 Logger,Must()函数如果返回值不是 nil,就会报 panic。也就是检查Build是否错误 logger := zap.Must(cfg.Build()) defer logger.Sync() logger.Info("logger construction succeeded") } /* Must() 函数 // var logger = zap.Must(zap.NewProduction()) func Must(logger *Logger, err error) *Logger { if err != nil { panic(err) } return logger } */
consol 输出如下:
{"level":"info","message-customer":"logger construction succeeded","foo":"bar"}
并且在程序目录下生成了一个文件 logs.log,里面记录的日志内容也是上面consol输出内容。每运行一次就在日志文件末尾append一次内容。
例子2:高级配置
上面的配置只是基本的自定义配置,如果有一些复杂的需求,比如在多个文件之间分割日志。
或者输出到不是 file 的文件中,比如输出到 kafka 中,那么就需要使用 zapcore 包。
在下面的例子中,我们将把日志输出到 kafka 中,并且也输出到 console 里。并且我们对 kafka 不同主题进行编码设置,对输出到 console 编码进行设置,也希望处理高优先级的日志。
官方例子:
package main import ( "io" "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { // 首先,定义不同级别日志处理逻辑 highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel }) lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl < zapcore.ErrorLevel }) // 假设有2个kafka 的 topic,一个 debugging,一个 errors // zapcore.AddSync 添加一个文件句柄。 topicDebugging := zapcore.AddSync(io.Discard) topicErrors := zapcore.AddSync(io.Discard) // 如果他们对并发使用不安全,我们可以用 zapcore.Lock 添加一个 mutex 互斥锁。 consoleDebugging := zapcore.Lock(os.Stdout) consoleErrors := zapcore.Lock(os.Stderr) // 设置 kafka 和 console 输出配置 kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) // 把上面的设置加入到 zapcore.NewCore() 函数里,然后再把他们加入到 zapcore.NewTee() 函数里 core := zapcore.NewTee( zapcore.NewCore(kafkaEncoder, topicErrors, highPriority), zapcore.NewCore(consoleEncoder, consoleErrors, highPriority), zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority), zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), ) // 最后调用 zap.New() 函数 logger := zap.New(core) defer logger.Sync() logger.Info("constructed a logger") }
例子3:日志写入文件
与上面例子2相似,但是比它简单
package main import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { writetofile() } func writetofile() { // 设置一些配置参数 config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewJSONEncoder(config) defaultLogLevel := zapcore.DebugLevel // 设置 loglevel logFile, _ := os.OpenFile("./log-test-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 06666) // or os.Create() writer := zapcore.AddSync(logFile) logger := zap.New( zapcore.NewCore(fileEncoder, writer, defaultLogLevel), zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), ) defer logger.Sync() url := "http://www.test.com" logger.Info("write log to file", zap.String("url", url), zap.Int("attemp", 3), ) }
例子4:根据日志级别写入不同文件
这个与上面例子2相似
package main import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { writeToFileWithLogLevel() } func writeToFileWithLogLevel() { // 设置配置 config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewJSONEncoder(config) logFile, _ := os.OpenFile("./log-debug-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日志记录debug信息 errFile, _ := os.OpenFile("./log-err-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日志记录error信息 teecore := zapcore.NewTee( zapcore.NewCore(fileEncoder, zapcore.AddSync(logFile), zap.DebugLevel), zapcore.NewCore(fileEncoder, zapcore.AddSync(errFile), zap.ErrorLevel), ) logger := zap.New(teecore, zap.AddCaller()) defer logger.Sync() url := "http://www.diff-log-level.com" logger.Info("write log to file", zap.String("url", url), zap.Int("time", 3), ) logger.With( zap.String("url", url), zap.String("name", "jimmmyr"), ).Error("test error ") }
主要是设置日志级别,和把 2 个设置的 NewCore 放入到方法 NewTee 中。
六、Hook和Namespace
Hook (钩子函数)回调函数为用户提供一种简单方法,在每次日志内容记录后运行这个回调函数,执行用户需要的操作。也就是说记录完日志后你还想做其它事情就可以调用这个函数。
package main import ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { logger := zap.NewExample(zap.Hooks(func(entry zapcore.Entry) error { fmt.Println("[zap.Hooks]test Hooks") return nil })) defer logger.Sync() logger.Info("test output") logger.Warn("warn info") }
创建一个命名空间,后面的字段都在这名字空间中。Namespace 就像一个文件夹,后面文件都放在这个文件夹里。
package main import ( "go.uber.org/zap" ) func main() { logger := zap.NewExample() defer logger.Sync() logger.Info("some message", zap.Namespace("shop"), zap.String("name", "LiLei"), zap.String("grade", "No2"), ) logger.Error("some error message", zap.Namespace("shop"), zap.String("name", "LiLei"), zap.String("grade", "No3"), ) }
输出:
{"level":"info","msg":"some message","shop":{"name":"LiLei","grade":"No2"}} {"level":"error","msg":"some error message","shop":{"name":"LiLei","grade":"No3"}}
七、日志切割归档
lumberjack 这个库是按照日志大小切割日志文件。
安装 v2 版本:
go get -u github.com/natefinch/lumberjack@v2
Code:
log.SetOutput(&lumberjack.Logger{ Filename: "/var/log/myapp/foo.log", // 文件位置 MaxSize: 500, // megabytes,M 为单位,达到这个设置数后就进行日志切割 MaxBackups: 3, // 保留旧文件最大份数 MaxAge: 28, //days , 旧文件最大保存天数 Compress: true, // disabled by default,是否压缩日志归档,默认不压缩 })
参照它的文档和结合上面自定义配置的例子,写一个例子:
package main import ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) func main() { lumberjacklogger := &lumberjack.Logger{ Filename: "./log-rotate-test.json", MaxSize: 1, // megabytes MaxBackups: 3, MaxAge: 28, //days Compress: true, // disabled by default } defer lumberjacklogger.Close() config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder // 设置时间格式 fileEncoder := zapcore.NewJSONEncoder(config) core := zapcore.NewCore( fileEncoder, //编码设置 zapcore.AddSync(lumberjacklogger), //输出到文件 zap.InfoLevel, //日志等级 ) logger := zap.New(core) defer logger.Sync() // 测试分割日志 for i := 0; i < 8000; i++ { logger.With( zap.String("url", fmt.Sprintf("www.test%d.com", i)), zap.String("name", "jimmmyr"), zap.Int("age", 23), zap.String("agradege", "no111-000222"), ).Info("test info ") } }
八、其它方法使用
全局 Logger
zap提供了 2 种全局 Logger,一个是 zap.Logger,调用 zap.L() 获取;
另外一个是 zap.SugaredLogger ,调用 zap.S() 获取。
注意:直接调用 zap.L() 或 zap.S() 记录日志的话,它是不会记录任何日志信息。需要调用 ReplaceGlobals() 函数将它设置为全局 Logger。
ReplaceGlobals 替换全局 Logger 和 SugaredLogger,并返回一个函数来恢复原始值。
并发使用它是安全的。
看看 zap/global.go 中的源码:
// https://github.com/uber-go/zap/blob/v1.24.0/global.go var ( _globalMu sync.RWMutex _globalL = NewNop() _globalS = _globalL.Sugar() ) func L() *Logger { _globalMu.RLock() // 加了读锁,所以并发使用是安全的 l := _globalL _globalMu.RUnlock() return l } func S() *SugaredLogger { _globalMu.RLock() // 加了读锁,所以并发使用是安全的 s := _globalS _globalMu.RUnlock() return s } func ReplaceGlobals(logger *Logger) func() { _globalMu.Lock() prev := _globalL _globalL = logger _globalS = logger.Sugar() _globalMu.Unlock() return func() { ReplaceGlobals(prev) } // 返回一个函数类型 }
上面源码中的关键是 _globalL = NewNop() , NewNop 函数源码在 zap/logger.go 中,这个函数返回初始化了的一个 *Logger:
// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#L85 func NewNop() *Logger { return &Logger{ core: zapcore.NewNopCore(), errorOutput: zapcore.AddSync(io.Discard), addStack: zapcore.FatalLevel + 1, clock: zapcore.DefaultClock, } }
上面是源码简析,下面给出一个简单使用的例子。
简单使用例子:
package main import ( "go.uber.org/zap" ) func main() { // 直接调用是不会记录日志信息的,所以下面日志信息不会输出 zap.L().Info("no log info") zap.S().Info("no log info [sugared]") logger := zap.NewExample() defer logger.Sync() zap.ReplaceGlobals(logger) // 全局logger,zap.L() 和 zap.S() 需要调用 ReplaceGlobals 函数才会记录日志信息 zap.L().Info("log info") zap.S().Info("log info [sugared]") }
运行输出:
{"level":"info","msg":"log info"} {"level":"info","msg":"log info [sugared]"}
与标准日志库搭配
zap 提供了一个函数 NewStdLog
,可以把标准日志库 log 转换为 zap 的日志,这为我们从标准日志库转换到 zap 日志库的使用提供了简洁的转换操作。
例子:
package main import ( "go.uber.org/zap" ) func main() { logger := zap.NewExample() defer logger.Sync() std := zap.NewStdLog(logger) std.Print("standard logger wrapper") }
运行输出:
{"level":"info","msg":"standard logger wrapper"}
如果你还想设置日志级别,可以使用另外一个函数 NewStdLogAt
,它的第二个参数就是日志级别:
NewStdLogAt(l *Logger, level zapcore.Level) (*log.Logger, error)
一段代码中使用log另外的使用zap
zap 还提供了另外一个函数 RedirectStdLog
,它可以帮助我们在一段代码中使用标准日志库 log,其它地方还是使用 zap.Logger
。如下例子:
package main import ( "log" "go.uber.org/zap" ) func main() { logger := zap.NewExample() defer logger.Sync() undo := zap.RedirectStdLog(logger) log.Print("redirected standard library") undo() log.Print("this zap logger") }
输出:
{"level":"info","msg":"redirected standard library"} 2023/05/06 00:47:11 this zap logger
同样如果想增加日志级别,可以使用函数 RedirectStdLogAt
:
func RedirectStdLogAt(l *Logger, level zapcore.Level) (func(), error)
输出调用堆栈
主要是调用函数 zap.AddStacktrace()
,见下面例子:
package main import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func Hello() { Warn("hello", zap.String("h", "world"), zap.Int("c", 1)) } func Warn(msg string, fields ...zap.Field) { zap.L().Warn(msg, fields...) } func main() { logger, _ := zap.NewProduction(zap.AddStacktrace(zapcore.WarnLevel)) defer logger.Sync() zap.ReplaceGlobals(logger) Hello() }
运行输出:
{"level":"warn","ts":1683306442.3578277,"caller":"zapdemos/addstacktrace.go:13","msg":"hello","h":"world","c":1, "stacktrace": "main.Warn\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:13\n main.Hello\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:9\n main.main\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:22\n runtime.main\n\tE:/programfile/go/src/runtime/proc.go:250"}
输出文件名和行号
AddCaller 将 Logger 配置为使用 zap 调用者的文件名、行号和函数名称,把这些信息添加到日志记录中。它底层调用的是 WithCaller
。
addcaller.go:
package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction(zap.AddCaller()) defer logger.Sync() logger.Info("AddCaller:line No and filename") }
输出:
{"level":"info","ts":1683307204.6184027,"caller":"zapdemos/addcaller.go:11","msg":"AddCaller:line No and filename"}
logger.Info() 方法在第11行被调用。
zap 还提供了另外一个函数 zap.AddCallerSkip(skip int) Option
,可以设置向上跳几层,然后记录文件名和行号。向上跳几层就是跳过调用者的数量。有时函数调用可能有嵌套,用这个函数可以定位到里面的函数。
addcallerskip.go
package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1)) defer logger.Sync() zap.ReplaceGlobals(logger) Hello() } func Hello() { Warn("hello", zap.String("h", "world"), zap.Int("c", 1)) } func Warn(msg string, fields ...zap.Field) { zap.L().Warn(msg, fields...) }
输出:
{"level":"warn","ts":1683308118.1684704,"caller":"zapdemos/addcallerskip.go:17","msg":"hello","h":"world","c":1}
日志中的 17 表示 Hello() 函数里的 Warn() 的行号。
如果 zap.AddCallerSkip(2)
,日志中显示行号为 13,表示 Hello() 的行号。
九、zap使用总结
- zap 的使用,先创建 logger,再调用各个日志级别方法记录日志信息。比如 logger.Info()。
- zap 提供了三种快速创建 logger 的方法:
zap.Newproduction()
,zap.NewDevelopment()
,zap.NewExample()
。见名思义,Example 一般用在测试代码中,Development 用在开发环境中,Production 用在生成环境中。这三种方法都预先设置好了配置信息。它们的日志数据类型输出都是强类型。 - 当然,zap 也提供了给用户自定义的方法
zap.New()
。比如用户可以自定义一些配置信息等。 - 在上面的例子中,几乎都有
defer logger.Sync()
这段代码,为什么?因为 zap 底层 API 允许缓冲日志以提高性能,在默认情况下,日志记录器是没有缓冲的。但是在进程退出之前调用Sync()
方法是一个好习惯。 - 如果你在 zap 中使用了 sugaredlogger,把 zap 创建 logger 的三种方法用
logger.Sugar()
包装下,那么 zap 就支持 printf 风格的格式化输出,也支持以 w 结尾的方法。如 Infow,Infof 等。这种就是通用类型日志输出,不是强类型输出,不需要强制指定输出的数据类型。它们的性能区别,通用类型会比强类型下降 50% 左右。
比如 Infow 的输出形式,Infow 不需要 zap.String 这种指定字段的数据类型。如下代码:
sugar := logger.Sugar() sugar.Infow("failed to fetch URL", "url", url, "attempt", 3, "backoff", time.Second, )
强类型输出,比如 Info 方法输出字段和值就需要指定数据类型:
logger.Info("failed to fetch url", // 强类型字段 zap.String("url", "http://example.com"), zap.Int("attempt", 3), zap.Duration("backoff", time.Second), )
强类型输出和通用类型输出区别
通用类型输出,经过 interface{} 转换会有性能损失,标准库的 fmt.Printf 为了通用性就用了 interface{} 这种”万能型“的数据类型,另外它还使用了反射,性能进一步降低。
zap 强类型输出,zap 为了提供日志输出性能,zap 的强类型输出没有使用 interface{} 和反射。zap 默认输出就是强类型。
上面介绍,zap 中 3 种创建 logger 方式(zap.Newproduction()
,zap.NewDevelopment()
,zap.NewExample()
)就是强类型日志字段,当然,也可以转化为通用类型,用 logger.Sugar()
方法创建 SugaredLogger。
zap.Namespace()
创建一个命名空间,后面的字段都在这名字空间中。Namespace 就像一个文件夹,后面文件都放在这个文件夹里。
logger.Info("some message", zap.Namespace("shop"), zap.String("shopid", "s1234323"), )
{"level":"info","msg":"some message","shop":{"shopid":"s1234323"}}
十、Demo源码地址
https://github.com/jiujuan/go-exercises/tree/main/zapdemos
以上就是Golang日志操作库zap的使用详解的详细内容,更多关于Go日志操作库zap的资料请关注脚本之家其它相关文章!