返回

Go第三方log库之zap

Go第三方log库之zap使用。


zap 是由 Uber 公司开源的一款 Go 日志库,就像它的命名一样,zap 以快著称。官方 GitHub 仓库中只用一句话来概括 zap:「在 Go 中进行快速、结构化、分级的日志记录」。这句话简单明了的概括了 zap 的核心特性,今天我们就来介绍下 zap 日志库的基本使用和高级特性,以及如何在实际应用程序中使用,来提高应用程序的可靠性。


特点

zap 具有如下特点:

  • 快,非常快,这也是 zap 最显著的特点。速度快的原因是 zap 避免使用 interface{} 和反射,并且使用 sync.Pool 减少堆内存分配。在 zap 面前 Logrus 的执行速度只有被吊打的份,你可以在官方 GitHub 仓库中看到 zap 与不同日志库的速度对比。
  • 支持结构化日志记录。这是一个优秀的日志库必备功能。
  • 支持七种日志级别:DebugInfoWarnErrorDPanicPanicFatal,其中 DPanic 是指在开发环境下(development)记录日志后会进行 panic
  • 支持输出调用堆栈。
  • 支持 Hooks 机制。

使用

基本使用

  • 先安装go get -u go.uber.org/zap

  • 基本使用如下:

    zap库的使用与其他的日志库非常相似。先创建一个logger,然后调用各个级别的方法记录日志(Debug/Info/Warn/Error/)。zap提供了几个快速创建logger的方法,zap.NewExample()zap.NewDevelopment()zap.NewProduction(),还有高度定制化的创建方法zap.New()。创建前 3 个logger时,zap会使用一些预定义的设置,它们的使用场景也有所不同。Example适合用在测试代码中,Development在开发环境中使用,Production用在生成环境。

    zap底层 API 可以设置缓存,所以一般使用defer logger.Sync()将缓存同步到文件中。刷新缓存,确保日志输出。

    由于fmt.Printf之类的方法大量使用interface{}和反射,会有不少性能损失,并且增加了内存分配的频次。zap为了提高性能、减少内存分配次数,没有使用反射,而且默认的Logger只支持强类型的、结构化的日志。必须使用zap提供的方法记录字段。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也提供了便捷的方法SugarLogger,可以使用printf格式符的方式。调用logger.Sugar()即可创建SugaredLoggerSugaredLogger的使用比Logger简单,只是性能比Logger低 50% 左右,可以用在非热点函数中。调用SugarLoggerf结尾的方法与fmt.Printf没什么区别,如例子中的Infof。同时SugarLogger还支持以w结尾的方法,这种方式不需要先创建字段对象,直接将字段名和值依次放在参数中即可,如例子中的Infow

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    package main
    
    import (
        "fmt"
        "time"
    
        "go.uber.org/zap"
    )
    
    func main() {
        // 生产环境
        fmt.Println("------生产环境------")
        {
           logger, _ := zap.NewProduction()
           defer logger.Sync() // 刷新 buffer,保证日志最终会被输出
    
           url := "https://arlettebrook.github.io/"
           logger.Info("production failed to fetch URL",
              zap.String("url", url), // 因为没有使用 interface{} 和反射机制,所以需要指定具体类型
              zap.Int("attempt", 3),
              zap.Duration("backoff", time.Second),
           )
        }
    
        // 开发环境
        fmt.Println("------开发环境------")
        {
           logger, _ := zap.NewDevelopment()
           defer logger.Sync()
    
           url := "https://arlettebrook.github.io/"
           logger.Debug("development failed to fetch URL",
              zap.String("url", url),
              zap.Int("attempt", 3),
              zap.Duration("backoff", time.Second),
           )
        }
    
        // 测试环境
        fmt.Println("------测试环境------")
        {
           logger := zap.NewExample()
           defer logger.Sync()
    
           url := "https://arlettebrook.github.io/"
           logger.Info("failed to fetch URL",
              zap.String("url", url),
              zap.Int("attempt", 3),
              zap.Duration("backoff", time.Second),
           )
    
           fmt.Println("------sugaredLogger------")
           sugar := logger.Sugar()
           sugar.Infow("failed to fetch URL",
              "url", url,
              "attempt", 3,
              "backoff", time.Second,
           )
           sugar.Infof("Failed to fetch URL: %s", url)
        }
    }
    

    zap 针对生产环境、开发环境以及测试环境提供了不同的函数来创建 Logger 对象。

    如果想在日志后面追加 key-value,则需要根据 value 的数据类型使用 zap.Stringzap.Int 等方法实现。这一点在使用上显然不如 Logrus 等其他日志库来的方便,但这也是 zap 速度快的原因之一,zap 内部尽量避免使用 interface{} 和反射来提高代码执行效率。

    记录日志的 logger.Xxx 方法签名如下:

    1
    
    func (log *Logger) Info(msg string, fields ...Field)
    

    其中 fieldszapcore.Field 类型,用来存储 key-value,并记录 value 类型,不管是 zap.String 还是 zap.Int 底层都是 zapcore.Field 类型来记录的。zap 为每一种 Go 的内置类型都定义了对应的 zap.Xxx 方法,甚至还实现 zap.Any() 来支持 interface{}

    执行以上代码,控制台得到如下输出:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    ------生产环境------
    {"level":"info","ts":1714375211.8196504,"caller":"learn/main.go:18","msg":"production failed to fe
    tch URL","url":"https://arlettebrook.github.io/","attempt":3,"backoff":1}
    ------开发环境------
    2024-04-29T15:20:11.820+0800    DEBUG   learn/main.go:32        development failed to fetch URL {"
    url": "https://arlettebrook.github.io/", "attempt": 3, "backoff": "1s"}
    ------测试环境------
    {"level":"info","msg":"failed to fetch URL","url":"https://arlettebrook.github.io/","attempt":3,"b
    ackoff":"1s"}
    ------sugaredLogger------
    {"level":"info","msg":"failed to fetch URL","url":"https://arlettebrook.github.io/","attempt":3,"b
    ackoff":"1s"}
    {"level":"info","msg":"Failed to fetch URL: https://arlettebrook.github.io/"}
    

    可以发现,通过 zap.NewProduction() 创建的日志对象输出格式为 JSON,而通过 zap.NewDevelopment() 创建的日志对象输出格式为 Text,日志后面追加的 key-value 会被转换成 JSON。并且,两者输出的字段内容也略有差异,如生产环境日志输出的时间格式为 Unix epoch 利于程序解析,而开发环境日志输出的时间格式为 ISO8601 更利于人类阅读。测试环境没有文件行号、堆栈跟踪信息以及时间,格式为JSON。对应的sugar都是是一样的。

    导致以上这些差异的原因是配置不同,我们来看下 zap.NewProductionzap.NewDevelopment 的代码实现:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    func NewProduction(options ...Option) (*Logger, error) {
    	return NewProductionConfig().Build(options...)
    }
    
    func NewProductionConfig() Config {
    	return Config{
    		Level:       NewAtomicLevelAt(InfoLevel),
    		Development: false,
    		Sampling: &SamplingConfig{
    			Initial:    100,
    			Thereafter: 100,
    		},
    		Encoding:         "json",
    		EncoderConfig:    NewProductionEncoderConfig(),
    		OutputPaths:      []string{"stderr"},
    		ErrorOutputPaths: []string{"stderr"},
    	}
    }
    
    func NewDevelopment(options ...Option) (*Logger, error) {
    	return NewDevelopmentConfig().Build(options...)
    }
    
    func NewDevelopmentConfig() Config {
    	return Config{
    		Level:            NewAtomicLevelAt(DebugLevel),
    		Development:      true,
    		Encoding:         "console",
    		EncoderConfig:    NewDevelopmentEncoderConfig(),
    		OutputPaths:      []string{"stderr"},
    		ErrorOutputPaths: []string{"stderr"},
    	}
    }
    

    可以看到,两者在实现思路上是一样的,都是先创建一个配置对象 zap.Config,然后再调用配置对象的 Build 方法来构建 Logger

    zap.Config 定义如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    type Config struct {
    	Level AtomicLevel `json:"level" yaml:"level"`
    	Development bool `json:"development" yaml:"development"`
    	DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    	DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    	Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    	Encoding string `json:"encoding" yaml:"encoding"`
    	EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    	OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    	ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    	InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
    }
    

    每个配置项说明如下:

    • Level: 日志级别。
    • Development: 是否为开发模式。
    • DisableCaller: 禁用调用信息,值为 true 时,日志中将不再显示记录日志时所在的函数调用文件名和行号。
    • DisableStacktrace: 禁用堆栈跟踪捕获。
    • Sampling: 采样策略配置,单位为每秒,作用是限制日志在每秒内的输出数量,以此来防止全局的 CPU 和 I/O 负载过高。
    • Encoding: 指定日志编码器,目前支持 jsonconsole
    • EncoderConfig: 编码配置,决定了日志字段格式。
    • OutputPaths: 配置日志输出位置,URLs 或文件路径,可配置多个。
    • ErrorOutputPaths: zap 包内部出现错误的日志输出位置,URLs 或文件路径,可配置多个,默认 os.Stderr
    • InitialFields: 初始化字段配置,该配置的字段会以结构化的形式打印在每条日志输出中。

    我们再来对比下 NewProductionEncoderConfig()NewDevelopmentEncoderConfig() 这两个配置的不同:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    func NewProductionEncoderConfig() zapcore.EncoderConfig {
    	return zapcore.EncoderConfig{
    		TimeKey:        "ts",
    		LevelKey:       "level",
    		NameKey:        "logger",
    		CallerKey:      "caller",
    		FunctionKey:    zapcore.OmitKey,
    		MessageKey:     "msg",
    		StacktraceKey:  "stacktrace",
    		LineEnding:     zapcore.DefaultLineEnding,
    		EncodeLevel:    zapcore.LowercaseLevelEncoder,
    		EncodeTime:     zapcore.EpochTimeEncoder,
    		EncodeDuration: zapcore.SecondsDurationEncoder,
    		EncodeCaller:   zapcore.ShortCallerEncoder,
    	}
    }
    
    func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
    	return zapcore.EncoderConfig{
    		// Keys can be anything except the empty string.
    		TimeKey:        "T",
    		LevelKey:       "L",
    		NameKey:        "N",
    		CallerKey:      "C",
    		FunctionKey:    zapcore.OmitKey,
    		MessageKey:     "M",
    		StacktraceKey:  "S",
    		LineEnding:     zapcore.DefaultLineEnding,
    		EncodeLevel:    zapcore.CapitalLevelEncoder,
    		EncodeTime:     zapcore.ISO8601TimeEncoder,
    		EncodeDuration: zapcore.StringDurationEncoder,
    		EncodeCaller:   zapcore.ShortCallerEncoder,
    	}
    }
    

    对比来看,两者有很多不同的配置,比如生产环境下 EncodeTime 值为 zapcore.EpochTimeEncoder,开发环境下 EncodeTime 值为 zapcore.ISO8601TimeEncoder。这就是生产环境日志输出的时间格式为 Unix epoch 而开发环境日志输出的时间格式为 ISO8601 的原因。

    zapcore.EncoderConfig 其他几个常用的配置项说明如下:

    • MessageKey: 日志信息的键名,默认 msg
    • LevelKey: 日志级别的键名,默认 level
    • TimeKey: 日志时间的键名。
    • EncodeLevel: 日志级别的格式,默认为小写,如 info

    除了提供 zap.NewProduction()zap.NewDevelopment() 两个构造函数外,zap 还提供了 zap.NewExample() 来创建一个 Logger 对象,这个方法主要用于测试,这里就不多介绍了。

记录层级关系

前面我们记录的日志都是一层结构,没有嵌套的层级。我们可以使用zap.Namespace(key string) Field构建一个命名空间,后续的Field都记录在此命名空间中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  logger.Info("tracked some metrics",
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )

  logger2 := logger.With(
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )
  logger2.Info("tracked some metrics")
}

输出:

1
2
{"level":"info","msg":"tracked some metrics","metrics":{"counter":1}}
{"level":"info","msg":"tracked some metrices","metrics":{"counter":1}}

上面我们演示了两种Namespace的用法,一种是直接作为字段传入Debug/Info等方法,一种是调用With()创建一个新的Logger,新的Logger记录日志时总是带上预设的字段。With()方法实际上是创建了一个新的Logger

1
2
3
4
5
6
7
8
9
// src/go.uber.org/zap/logger.go
func (log *Logger) With(fields ...Field) *Logger {
  if len(fields) == 0 {
    return log
  }
  l := log.clone()
  l.core = l.core.With(fields)
  return l
}

预设日志字段

如果每条日志都要记录一些共用的字段,那么使用zap.Fields(fs ...Field)创建的选项。例如在服务器日志中记录可能都需要记录serverIdserverName

1
2
3
4
5
6
7
8
func main() {
  logger := zap.NewExample(zap.Fields(
    zap.Int("serverId", 90),
    zap.String("serverName", "awesome web"),
  ))

  logger.Info("hello world")
}

输出:

1
{"level":"info","msg":"hello world","serverId":90,"serverName":"awesome web"}

logger.with()差不多

给语法加点糖

zap 虽然速度足够快,但是多数情况下,我们并不需要极致的性能,而是想让代码写起来更爽一些。zap 为我们提供了解决方案 —— SugaredLogger

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.NewProduction()
	defer logger.Sync()

	url := "https://arlettebrook.github.io/"
	sugar := logger.Sugar()
	sugar.Infow("production failed to fetch URL",
		"url", url,
		"attempt", 3,
		"backoff", time.Second,
	)
	sugar.Info("Info")
    sugar.Infoln("Infoln")
	sugar.Infof("Infof: %s", url)
}

通过 logger.Sugar() 方法可以将一个 Logger 对象转换成一个 SugaredLogger 对象。

SugaredLogger 提供了更人性化的接口,日志中追加 key-value 时不在需要 zap.String("url", url) 这种显式指明类型的写法,只需要保证 key 为 string 类型,value 则可以为任意类型,能够减少我们编写的代码量。

此外,为了满足不同需求,SugaredLogger 提供了四种方式输出日志:sugar.Xxxsugar.Xxxwsugar.Xxxfsugar.Xxxln

执行以上代码,控制台得到如下输出:

1
2
3
4
5
6
{"level":"info","ts":1714398451.8505704,"caller":"learn/main.go:15","msg":"production failed to fe
tch URL","url":"https://arlettebrook.github.io/","attempt":3,"backoff":1}
{"level":"info","ts":1714398451.8511178,"caller":"learn/main.go:20","msg":"Info"}
{"level":"info","ts":1714398451.851623,"caller":"learn/main.go:21","msg":"Infoln"}
{"level":"info","ts":1714398451.8516397,"caller":"learn/main.go:22","msg":"Infof: https://arletteb
rook.github.io/"}

我们知道,这种方便的写法是有一定代价的,所以开发中是否需要使用 SugaredLogger 来记录日志,需要根据程序的特点来决定。SugaredLoggerLogger 的性能对比同样可以在官方 GitHub 仓库中看到。

定制 Logger

通过查看 zap.NewProduction()zap.NewDevelopment() 两个构造函数源码,我们知道可以使用 zap.Config 对象的 Build 方法创建 Logger 对象。那么我们很容易能够想到,如果要定制 Logger,只需要创建一个定制的 zap.Config 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

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

func newCustomLogger() (*zap.Logger, error) {
	cfg := zap.Config{
		Level:       zap.NewAtomicLevelAt(zap.DebugLevel),
		Development: false,
		Encoding:    "json",
		EncoderConfig: zapcore.EncoderConfig{
			TimeKey:        "time",
			LevelKey:       "level",
			NameKey:        "logger",
			CallerKey:      "", // 不记录日志调用位置
			FunctionKey:    zapcore.OmitKey,
			MessageKey:     "message",
			LineEnding:     zapcore.DefaultLineEnding,
			EncodeLevel:    zapcore.LowercaseLevelEncoder,
			EncodeTime:     zapcore.RFC3339TimeEncoder,
			EncodeDuration: zapcore.SecondsDurationEncoder,
			EncodeCaller:   zapcore.ShortCallerEncoder,
		},
		OutputPaths:      []string{"stdout", "test.log"},
		ErrorOutputPaths: []string{"error.log"},
	}
	return cfg.Build()
}

func main() {
	logger, _ := newCustomLogger()
	defer logger.Sync()

	// 增加一个 skip 选项,触发 zap 内部 error,将错误输出到 error.log
	logger = logger.WithOptions(zap.AddCallerSkip(100))

	logger.Info("Info msg")
	logger.Error("Error msg")
}

以上代码通过 newCustomLogger 函数创建了一个自定义的 Logger,同样通过先定义一个 zap.Config 然后再调用其 Build 方法来实现。

配置日志分别输出到标准输出和 test.log 文件,执行以上代码,控制台和 test.log 都会得到如下输出:

1
2
{"level":"info","time":"2023-03-19T19:19:18+08:00","message":"Info msg"}
{"level":"error","time":"2023-03-19T19:19:18+08:00","message":"Error msg"}

另外,我们还通过 logger.WithOptions()Logger 对象增加了一个选项 zap.AddCallerSkip(100),这个选项的作用是指定在通过调用栈获得行号时跳过的调用深度,因为我们的函数调用栈并不是 100 层,所以会触发 zap 内部错误,zap 会将错误日志输出到 ErrorOutputPaths 配置指定的位置中,即 error.log

error.log 得到的错误日志如下:

1
2
2023-03-19 11:19:18.438824 +0000 UTC Logger.check error: failed to get caller
2023-03-19 11:19:18.44921 +0000 UTC Logger.check error: failed to get caller

选项

logger.WithOptions() 支持的选项如下:

  • WrapCore(f func(zapcore.Core) zapcore.Core): 使用一个新的 zapcore.Core 替换掉 Logger 内部原有的的 zapcore.Core 属性。
  • Hooks(hooks ...func(zapcore.Entry) error): 注册钩子函数,用来在日志打印时同时调用注册的钩子函数。
  • Fields(fs ...Field): 添加公共字段。
  • ErrorOutput(w zapcore.WriteSyncer): 指定日志组件内部出现异常时的输出位置。
  • Development(): 将日志记录器设为开发模式,这将使 DPanic 级别日志记录错误后执行 panic()
  • AddCaller(): 与 WithCaller(true) 等价。
  • WithCaller(enabled bool): 指定是否在日志输出内容中增加调用信息,即文件名和行号。
  • AddCallerSkip(skip int): 指定在通过调用栈获取文件名和行号时跳过的调用深度。
  • AddStacktrace(lvl zapcore.LevelEnabler): 用来指定某个日志级别及以上级别输出调用堆栈。
  • IncreaseLevel(lvl zapcore.LevelEnabler): 提高日志级别,如果传入的 lvl 比现有级别低,则不会改变日志级别。
  • WithFatalHook(hook zapcore.CheckWriteHook): 当出现 Fatal 级别日志时调用的钩子函数。
  • WithClock(clock zapcore.Clock): 指定日志记录器用来确定当前时间的 zapcore.Clock 对象,默认为 time.Now 的系统时钟。

NewExample()/NewDevelopment()/NewProduction()这 3 个函数可以传入若干类型为zap.Option的选项,从而定制Logger的行为。又一次见到了选项模式!!

zap提供了丰富的选项供我们选择。

  • 输出文件名和行号

    调用zap.AddCaller()返回的选项设置输出文件名和行号。但是有一个前提,必须设置配置对象Config中的CallerKey字段。也因此NewExample()不能输出这个信息(它的Config没有设置CallerKey)。

    AddCaller()zap.WithCaller(true)等价。一般不用

    有时我们稍微封装了一下记录日志的方法,但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层。可能会用到。

  • 输出调用堆栈

    有时候在某个函数处理中遇到了异常情况,因为这个函数可能在很多地方被调用。如果我们能输出此次调用的堆栈,那么分析起来就会很方便。我们可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)达成这个目的。该函数指定lvl和之上的级别都需要输出调用堆栈:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    package main
    
    import (
        "go.uber.org/zap"
        "go.uber.org/zap/zapcore"
    )
    
    func f1() {
        f2("hello world")
    }
    
    func f2(msg string, fields ...zap.Field) {
        zap.L().Warn(msg, fields...) // zap.L()获取全局logger
    }
    
    func main() {
        logger, _ := zap.NewDevelopment(zap.AddStacktrace(zapcore.WarnLevel))
        defer logger.Sync()
    
        zap.ReplaceGlobals(logger) // 替换全局logger
    
        f1()
    }
    

    zapcore.WarnLevel传入AddStacktrace(),之后Warn()/Error()等级别的日志会输出堆栈,Debug()/Info()这些级别不会。运行结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    2024-04-30T10:32:49.798+0800    WARN    learn/main.go:13        hello world
    main.f2
            F:/GoProject/learn/main.go:13
    main.f1
            F:/GoProject/learn/main.go:9
    main.main
            F:/GoProject/learn/main.go:22
    runtime.main
            D:/Go/src/runtime/proc.go:271
    

    很清楚地看到调用路径。

创建自定义的配置对象,除了在代码中指定配置参数,也可以将这些配置项写入到 JSON 文件中,然后通过 json.Unmarshal 的方式将配置绑定到 zap.Config,可以参考官方示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
  rawJSON := []byte(`{
    "level":"debug",
    "encoding":"json",
    "outputPaths": ["stdout", "server.log"],
    "errorOutputPaths": ["stderr"],
    "initialFields":{"name":"dj"},
    "encoderConfig": {
      "messageKey": "message",
      "levelKey": "level",
      "levelEncoder": "lowercase"
    }
  }`)

  var cfg zap.Config
  if err := json.Unmarshal(rawJSON, &cfg); err != nil {
    panic(err)
  }
  logger, err := cfg.Build()
  if err != nil {
    panic(err)
  }
  defer logger.Sync()

  logger.Info("server start work successfully!")
}

上面创建一个输出到标准输出stdout和文件server.logLogger。观察输出:

1
{"level":"info","message":"server start work successfully!","name":"dj"}

全局Logger

为了方便使用,zap提供了两个全局的Logger,一个是*zap.Logger,可调用zap.L()获得;另一个是*zap.SugaredLogger,可调用zap.S()获得。需要注意的是,全局的Logger默认并不会记录日志!它是一个无实际效果的Logger。看源码:

1
2
3
4
5
6
// go.uber.org/zap/global.go
var (
  _globalMu sync.RWMutex
  _globalL  = NewNop()
  _globalS  = _globalL.Sugar()
)

我们可以使用ReplaceGlobals(logger *Logger) func()logger设置为全局的Logger,该函数返回一个无参函数,用于恢复全局Logger设置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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")
}

输出:

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

可以看到在调用ReplaceGlobals之前记录的日志并没有输出。

与标准日志库搭配使用

如果项目一开始使用的是标准日志库log,后面想转为zap。这时不必修改每一个文件。我们可以调用zap.NewStdLog(l *Logger) *log.Logger返回一个标准的log.Logger,内部实际上写入的还是我们之前创建的zap.Logger

1
2
3
4
5
6
7
func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  std := zap.NewStdLog(logger)
  std.Print("standard logger wrapper")
}

输出:

1
{"level":"info","msg":"standard logger wrapper"}

很方便不是吗?我们还可以使用NewStdLogAt(l *logger, level zapcore.Level) (*log.Logger, error)让标准接口以level级别写入内部的*zap.Logger

如果我们只是想在一段代码内使用标准日志库log,其它地方还是使用zap.Logger。可以调用RedirectStdLog(l *Logger) func()。它会返回一个无参函数恢复设置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  undo := zap.RedirectStdLog(logger)
  log.Print("redirected standard library")
  undo()

  log.Print("restored standard library")
}

看前后输出变化:

1
2
{"level":"info","msg":"redirected standard library"}
2020/04/24 22:13:58 restored standard library

当然RedirectStdLog也有一个对应的RedirectStdLogAt以特定的级别调用内部的*zap.Logger方法。

参考

  1. Go 每日一库之 zap
  2. zap 源码: https://github.com/uber-go/zap
  3. zap 文档: https://pkg.go.dev/go.uber.org/zap
  4. Go 第三方 log 库之 zap 使用
  5. 如何基于 zap 封装一个更好用的日志库