Go语言高性能日志库-zap介绍

zap介绍

go语言作为一门自带电池(Batteries Included)的编程语言,标准库功能非常的全,但是标准库自带的log包package,功能太简陋,连基本的日志分级(Log Level)输出都支持,也无法输出类似json这样的结构化日志, log包无法作为生产环境日志输出来使用。对于一个需要长期运行的后端应用来说,记录应用运行过程的日志,供后续观察和分析问题必不可少。

了解go语言生态对日志的支持情况后,最后深入了解打车公司uber推出的日志库zap(github),zap强调性能,官方文档上说比市面上类似的日志库性要快4到10倍,同时zap为了性能zap 做了不少的取舍,比如以下几种情况对性能影响大,zap库尽量避免

  1. 基于反射的序列化
  2. 字符串格式化
  3. 避免使用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())