Go语言高性能日志库-zap介绍
zap介绍
go语言作为一门自带电池(Batteries Included)的编程语言,标准库功能非常的全,但是标准库自带的log包package,功能太简陋,连基本的日志分级(Log Level)输出都支持,也无法输出类似json这样的结构化日志, log包无法作为生产环境日志输出来使用。对于一个需要长期运行的后端应用来说,记录应用运行过程的日志,供后续观察和分析问题必不可少。
了解go语言生态对日志的支持情况后,最后深入了解打车公司uber推出的日志库zap(github),zap强调性能,官方文档上说比市面上类似的日志库性要快4到10倍,同时zap为了性能zap 做了不少的取舍,比如以下几种情况对性能影响大,zap库尽量避免
- 基于反射的序列化
- 字符串格式化
- 避免使用interface {}带来的拆箱装箱开销
zap代码实现过程中,为了性能考虑做到:
- 无反射
- 零分配
- JSON 编码器
- 日志记录器Logger尽量避免序列化和对象分配以及回收 从zap性能对比文档上看,zap比其它日志库性能高很多。
使用记录
一 安装依赖
go get -u go.uber.org/zap
二 使用logger打印日志 zap提供原生的Logger和快捷使用SugaredLogger,原生Logger性能更好,但是使用体验上SugaredLogger更方面,除非是一些性能非常关键的场景,不然官方建议大部分情况使用SugaredLogger 大部分场景 我们都应该使用SugaredLogger
logger, _ := zap.NewProduction()
defer logger.Sync() // flushes buffer, if any
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
// Structured context as loosely typed key-value pairs.
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)
func (s *SugaredLogger) Infow(msg string, keysAndValues ...interface{})
Infow等以w结尾的打印日志函数,打印日志msg和,后续可选的键值对 以json字符串打印到日志里
func (s *SugaredLogger) Infof(template string, args ...interface{})
以Infof等以f结尾打印日志函数,类似printf格式化参数打印日志
三 原生Logger和SugardLogger 一些非常注重性能的场景 使用原生Logger,它比SugaredLogger更快,更少的内存对象分配,但是使用时必须限定日志记录键值对value类型,比如String,Int等各种类型,支持的类型定义在zap/field.go里,下面是使用例子
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
四 日志Logger Buffered缓冲 zap默认记录日志不使用缓冲区,zap也提供底层API,设置启用缓冲区,启用应用退出前记得调用Sync()方法
defer logger.Sync()
五 SugardLogger和Logger可以根据自己的场景相互转换
logger := zap.NewExample()
defer logger.Sync()
sugar := logger.Sugar()
plain := sugar.Desugar()
六 zap日志使用配置 zap提供一个开箱即用的生产环境配置和开发环境配置
logger, err := zap.NewProduction() //生产环境配置
// logger, err := zap.NewDevelopmentConfig() 开发配置
// logger, err := zap.NewExample()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer logger.Sync()
logger.Info("This is a production config log example", zap.String("username", "JackMa"))
// Production
// {"level":"info","ts":1674984972.10138,"caller":"zaplog/zap_test.go:36","msg":"This is a production config log example","username":"JackMa"}
// Development
// 2023-01-29T21:41:03.362+0800 INFO zaplog/zap_test.go:37 This is a production config log example {"username": "JackMa"}
可以看到
- Production配置下整条日志以JSON格式输出,附加的日志元数据字段有level(日志级别),ts(时间戳,UNIX格式),caller(输出日志位置)三个
- Development配置下 日志以扁平文本格式输出,字段之间用空格分隔。
七 zap配置详解 上面展示了zap自带的Production和Development开箱即用配置,直接上代码加注释
// 调用NewProductionConfig()生成详细配置
func NewProduction(options ...Option) (*Logger, error) {
return NewProductionConfig().Build(options...)
}
func NewProductionConfig() Config {
return Config{
Level: NewAtomicLevelAt(InfoLevel),//打印Info级别以上日志
Development: false,
Sampling: &SamplingConfig{ //百分之百采样
Initial: 100,
Thereafter: 100,
},
Encoding: "json", //日志条目以json格式输出
EncoderConfig: NewProductionEncoderConfig(),//配置输出字段
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
//我们也看下DevelopmentConfig
func NewDevelopmentConfig() Config {
return Config{
Level: NewAtomicLevelAt(DebugLevel),//打印Debug级别以上日志
Development: true,
Encoding: "console", //日志条目以扁片的文本行格式输出
EncoderConfig: NewDevelopmentEncoderConfig(), //配置输出字段
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
//再看看NewExample
func NewExample(options ...Option) *Logger {
encoderCfg := zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "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...)
}
Config结构体
type Config struct {
Level AtomicLevel //日志级别
Development bool //是否开发模式
DisableCaller bool //是否不记录调用者
DisableStacktrace bool // completely disables automatic stacktrace capturing
Sampling *SamplingConfig //采样配置,日志可以按比例丢弃
Encoding string //打印日志的格式,我们上面输出的日志是json格式,也可以传统Java日志库打印的扁平文本格式
EncoderConfig zapcore.EncoderConfig //配置输出JSON格式日志 字段名称
OutputPaths []string //输出日志文件路径
ErrorOutputPaths []string //zap内部的错误输出路径,默认是标准输出sysout
InitialFields map[string]interface{} //root logger字段
}
再看看EncoderConfig结构体,大部分字段根据名字能猜到用途,我就不加详细注释了
type EncoderConfig struct {
// Set the keys used for each log entry. If any key is empty, that portion
// of the entry is omitted.
MessageKey string `json:"messageKey" yaml:"messageKey"`
LevelKey string `json:"levelKey" yaml:"levelKey"`
TimeKey string `json:"timeKey" yaml:"timeKey"`
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"`
// Configure the primitive representations of common complex types. For
// example, some users may want all time.Times serialized as floating-point
// seconds since epoch, while others may prefer ISO8601 strings.
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// Unlike the other primitive type encoders, EncodeName is optional. The
// zero value falls back to FullNameEncoder.
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// Configure the encoder for interface{} type objects.
// If not provided, objects are encoded using json.Encoder
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// Configures the field separator used by the console encoder. Defaults
// to tab.
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}
八 zap日志的核心配置 从Example例子可以看到zapcore.NewCore()三个参数分别为
Encoder
编码WriteSyncer
输出目的LevelEnabler
日志记录级别
九 日志输出到文件配置
从“八 zap的日志核心配置”,第二个参数可以看到,日志输出配置为NewCore的第二个参数,参数类型定义如下:
type WriteSyncer interface {
io.Writer
Sync() error
}
同时提供一个AddSync函数,提供从io.Writer到WriterSyncer转换
func AddSync(w io.Writer) WriteSyncer{
switch w := w.(type) {
case WriteSyncer:
return w
default:
return writerWrapper{w}
}
}
我们可以通过以下两行代码创建一个写日志到文件的WriterSyncer
file, _ := os.Create("./test.log")
return zapcore.AddSync(file)
十 zap日志文件切分 作为多年的Java开发经验,第一次看到zap库尽然没自带日志文件切分功能,这个基础功能竟然没提供,还是大吃一惊(😱😱),需要再整合其她第三方包,按照zap的FAQ文档推荐的用lumberjack库做日志切分,好家伙,那我们就选用lumberjack
go get -u github.com/natefinch/lumberjack v0.0.0-20230119042236-215739b3bcdc
比如go语言标准库自带log设置输出日志到文件,并切分,配置如下
hook := lumberjack.Logger{
Filename: "./logs/applog" + ".log",
MaxSize: 1, //日志最大的大小(M)
MaxBackups: 10, //备份个数
MaxAge: 7, //最大保存天数(day)
Compress: true, //是否压缩
LocalTime: false,
}
log.SetOutput(&hook)
创建zapcore的WriteSyncer对象,支持日志文件拆分
// 使用lumberjack实现日志切割
func getRorateWritter() zapcore.WriteSyncer {
hook := lumberjack.Logger{
Filename: "./logs/app_rotate_log" + ".log",
MaxSize: 1, //日志最大的大小(M)
MaxBackups: 10, //备份个数
MaxAge: 7, //最大保存天数(day)
Compress: true, //是否压缩
LocalTime: false,
}
return zapcore.AddSync(&hook)
}
创建带日志切分功能的zap logger
rorateWritter := getRorateWritter()
encoder := getEncoder()
core := zapcore.NewCore(encoder, rorateWritter, zapcore.DebugLevel)
logger = zap.New(core, zap.AddCaller())