zap日志框架

github:zap

Hello World

package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger := zap.NewExample()
	defer logger.Sync() // zap底层 API 可以设置缓存,所以一般使用defer logger.Sync()将缓存同步到文件中

	url := "http://example.org/api"
	logger.Info("failed to fetch URL",
		zap.String("url", url),
		zap.Int("attempt", 3),
		zap.Duration("backoff", time.Second),
	)
}

由于fmt.Printf之类的方法大量使用interface{}和反射,会有不少性能损失,并且增加了内存分配的频次。zap为了提高性能、减少内存分配次数,没有使用反射,而且默认的Logger只支持强类型的、结构化的日志。必须使用zap提供的方法记录字段。

比如上述代码中的:

zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),

zap为 Go 语言中所有的基本类型和其他常见类型都提供了方法。这些方法的名称也比较好记忆,zap.TypeTypebool/int/uint/float64/complex64/time.Time/time.Duration/error等)就表示该类型的字段,zap.Typepp结尾表示该类型指针的字段,zap.Typess结尾表示该类型切片的字段。如:

  • zap.Bool(key string, val bool) Fieldbool字段

  • zap.Boolp(key string, val *bool) Fieldbool指针字段

  • zap.Bools(key string, val []bool) Fieldbool切片字段

其他特殊类型:

  • zap.Any(key string, value interface{}) Field:任意类型的字段

  • zap.Binary(key string, val []byte) Field:二进制串的字段

每个字段都使用方法包裹一层比较繁琐,所以zap提供了SugaredLogger,使用logger.Sugar()即可创建SugaredLogger

SugaredLogger

sugared的含义为糖,SugaredLogger的含义就是对Logger包装了一层语法糖,内部提供两种后缀的方法:

  • Infow,字段形式输出

  • Infofprintf方式输出

如下:

package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger := zap.NewExample()
	defer logger.Sync() // zap底层 API 可以设置缓存,所以一般使用defer logger.Sync()将缓存同步到文件中

	sugarLogger := logger.Sugar()
	url := "http://example.org/api"

	// 字段方式输出
	sugarLogger.Infow("failed to fetch URL",
		"url", url,
		"attempt", 3,
		"backoff", time.Second)

	// printf 方式输出
	sugarLogger.Infof("failed to fetch URL %s, %d, %d", url, 3, time.Second)

}

输出结果:

{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"failed to fetch URL http://example.org/api, 3, 1000000000"}

日志级别

zap共提供5中日志级别,分别为:

  • Debug

  • Info

  • Warn

  • Error,会打印出堆栈信息

  • Fatal,会打印出堆栈信息,并且调用os.Exit(1)退出程序

package main

import (
	"fmt"
	"go.uber.org/zap"
)

func main() {
	var logger *zap.Logger
	logger, _ = zap.NewProduction()

	logger.Debug("i am debug")       // 这行不会打印,因为NewProduction默认日志级别是 INFO
	logger.Info("i am info")         // INFO  级别日志,这个会正常打印
	logger.Warn("i am warn")         // WARN  级别日志,这个会正常打印
	logger.Error("i am error")       // ERROR 级别日志,这个会打印,并附带堆栈信息
	logger.Fatal("i am fatal")       // FATAL 级别日志,这个会打印,附带堆栈信息,并调用 os.Exit 退出
	fmt.Println("can i be printed?") // 这行不会打印,因为程序已经退出
}

运行结果:

{"level":"info","ts":1631939779.76289,"caller":"git-knowledge/main.go:13","msg":"i am info"}
{"level":"warn","ts":1631939779.762945,"caller":"git-knowledge/main.go:14","msg":"i am warn"}
{"level":"error","ts":1631939779.762951,"caller":"git-knowledge/main.go:15","msg":"i am error","stacktrace":"main.main\n\t/Users/yangsx/Project/git-knowledge/main.go:15\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:225"}
{"level":"fatal","ts":1631939779.7629771,"caller":"git-knowledge/main.go:16","msg":"i am fatal","stacktrace":"main.main\n\t/Users/yangsx/Project/git-knowledge/main.go:16\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:225"}

预定义配置Logger

zap提供了三个方法,用于快速创建logger

  • zap.NewExample():json格式,debug级别,适合用于测试代码中

  • zap.NewDevelopment():console格式,debug级别,人性化显示,用于开发环境

  • zap.NewProduction():json格式,info级别,方便解析,用于生产环境

  • zap.NewNop():不记录任何日志,直接丢弃

选项模式

NewExample()/NewDevelopment()/NewProduction()这 3 个函数可以传入若干类型为zap.Option的选项,从而定制Logger的行为。

option: 输出文件名以及行号

package main

import (
	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.NewProduction(zap.AddCaller()) // 添加caller 调用位置
	defer logger.Sync()
	// do something
	logger.Info("hello world")
}

打印日志:

{"level":"info","ts":1631952102.073369,"caller":"git-knowledge/main.go:11","msg":"hello world"}

这里,字段caller打印的调用位置是main.go:11行,但是这行一般是打印语句执行的位置,定位到这一行通常没有什么用,需要向前查找。我们可以使用zap.AddCallerSkip(skip int)向上跳 1 层。

项目中一般会对日志框架调用进行封装,这时候,如果打印的行数是日志的行数,总会带来一定的困惑,所以,可以设置向上跳一层,查看方法调用方的行数,这样比较有用。

option: 输出调用堆栈

func f1() {
  f2("hello world")
}

func f2(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)

  f1()
}

option: 预设字段

func main() {
  logger := zap.NewExample(zap.Fields(
    zap.Int("serverId", 90),
    zap.String("serverName", "awesome web"),
  ))

  logger.Info("hello world")
}

自定义配置Logger

通常情况下,上面的三种配置往往达不到我们的要求,这时通常自定义Logger。使用zap.Config结构体创建一个新的Logger配置,并调用他的Build方法,构建出一个Logger。

zap.Config支持的字段如下:

type Config struct {
	// 日志级别
	Level AtomicLevel `json:"level" yaml:"level"`
	// Development puts the logger in development mode, which changes the
	// behavior of DPanicLevel and takes stacktraces more liberally.
	Development bool `json:"development" yaml:"development"`
	// 禁用caller,也就是日志打印的位置
	DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
	// 禁用堆栈信息
	DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`

	Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
  // Encoding 日志打印的格式,可以是 json 或者 console
	Encoding string `json:"encoding" yaml:"encoding"`
	// Encoding 配置
	EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
	// 输出路径,可以是多个,可以是标准输出 stdout 或者 文件路径
	OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
	// 错误输出路径,可以是多个
	ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
	// 每条日志中都会输出这些值,也就是增加额外的字段
	InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

EncoderConfig的具体信息:

type EncoderConfig struct {
  // 日志中信息的键名,默认为msg
  MessageKey    string `json:"messageKey" yaml:"messageKey"`
  // 日志中级别的键名,默认为level
  LevelKey      string `json:"levelKey" yaml:"levelKey"`
  // 日志时间的键名,默认为 ts
  TimeKey       string `json:"timeKey" yaml:"timeKey"`
  // 日志名称的键名,默认为 name
  NameKey       string `json:"nameKey" yaml:"nameKey"`
  // 日志调用位置的键名, 默认为 caller
  CallerKey     string `json:"callerKey" yaml:"callerKey"`
  // 堆栈信息的键名, 默认为 stacktrace
  StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
  // 换行符格式,默认是 \n
  LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
  
  // 日志中级别的格式,默认为小写,如debug/info。 LevelEncoder 是一个闭包函数,以下都是,需要传入相应的实现,来完成encode的操作
  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"`
}

此外,针对各种Encoder,zap提供了一些内置的实现:

LevelEncoder:
- zapcore.LowercaseLevelEncoder // 小写
- zapcore.LowercaseColorLevelEncoder // 小写有颜色
- zapcore.CapitalLevelEncoder // 大写
- zapcore.CapitalColorLevelEncoder 大写有颜色

TimeEncoder: 
- zapcore.EpochTimeEncoder // unix 时间戳秒
- zapcore.EpochMillisTimeEncoder // unix时间戳毫秒
- zapcore.EpochNanosTimeEncoder // 纳秒
- zapcore.ISO8601TimeEncoder   // ISO8601格式显示时间
- zapcore.RFC3339TimeEncoder   // RFC3339格式显示时间
- RFC3339NanoTimeEncoder

DurationEncoder:
- SecondsDurationEncoder  // 秒数转换
- NanosDurationEncoder
- MillisDurationEncoder
- StringDurationEncoder

CallerEncoder:
- FullCallerEncoder // /full/path/to/package/file:line
- ShortCallerEncoder // package/file:line


NameEncoder:
- FullNameEncoder

全局Logger

为了方便在项目中使用,zap提供了两个全局Logger,分别是

  • 全局的*zap.Logger对象,可以通过zap.L()获取

  • 全局的*zap.SugaredLogger对象,可以通过zap.S()获取

注意,这两个Logger默认不记录任何日志,因为他们都是调用zap.NewNop()方法生成的,要想使用全局Logger,需要使用zap.ReplaceGlobals(logger)函数替换全局Logger:

package main

import (
	"go.uber.org/zap"
)

func main() {
	zap.L().Info("global Logger before")
	zap.S().Info("global SugaredLogger before")

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

	zap.ReplaceGlobals(logger)
	zap.L().Info("global Logger after")
	zap.S().Info("global SugaredLogger after")
}

输出:

{"level":"info","msg":"global Logger after"}
{"level":"info","msg":"global SugaredLogger after"}

思考:这里可以使用全局Logger来封装项目的统一Logger。建立一个package,存放全局Logger配置,然后暴露统一的日志打印方法,既可以与日志框架分离,又可以集中控制日志配置

最后更新于