使用 Zap 实现高性能日志记录

前言

Zap 是一款由 uber-go 开发的开源日志库,它支持多种日志级别和结构化,并对性能和内存分配做了极致的优化。

试用期大作业的后端中我们使用了 Zap 来进行日志记录,今日探索日志滚动时顺带回顾了一下 Zap 的基本用法,于是决定水一篇博客(逃

项目地址:https://github.com/uber-go/zap

快速使用

go get -u go.uber.org/zap

Zap 库的使用与其他的日志库非常相似,我们需要先创建一个日志记录器,然后调用相应的方法来记录不同级别的日志。

Zap 提供了两种日志记录器:Sugared LoggerLogger.

  • Sugared Logger 并重性能与易用性,支持结构化和 printf 风格的日志记录。

  • Logger 非常强调性能,不提供 printf 风格的 api,只支持强类型的、结构化的日志记录。

举例如下:

func main() {
    // Sugared Logger
    sugar := zap.NewExample().Sugar()
    sugar.Infof("hello! name:%s,age:%d", "xiaomin", 20) // printf 风格

    // Logger
    logger := zap.NewExample()
    logger.Info("hello!", zap.String("name", "xiaomin"), zap.Int("age", 20)) // 结构化
}

输出结果:

// output
{"level":"info","msg":"hello! name:xiaomin,age:20"}
{"level":"info","msg":"hello!","name":"xiaomin","age":20}

要创建一个 Logger,Zap 提供了三个默认配置:ExampleDevelopmentProduction,分别对应测试环境、开发环境和生产环境。

func main() {
   // Example
   logger := zap.NewExample()
   logger.Info("Example")

   // Development
   logger, _ = zap.NewDevelopment()
   logger.Info("Development")

   // Production
   logger, _ = zap.NewProduction()
   logger.Info("Production")
}

相应的输出如下:

// Example
{"level":"info","msg":"Example"}

// Development
2024-10-24T22:52:16.544+0800    INFO    ConfessionWallServer/main.go:14 Development

// Production
{"level":"info","ts":1729781536.5583117,"caller":"ConfessionWallServer/main.go:18","msg":"Production"}

可以看到,日志等级,日志输出格式,默认字段都有所差异。

定制 Logger

Zap 提供了丰富的配置选项。

使用自定义配置

我们可以通过自己创建 Config 来配置 Logger 的行为。

func main() {
    // 定制 Config
    config := zap.Config{
        Level:       zap.NewAtomicLevelAt(zap.InfoLevel), // 最低日志等级
        Encoding:    "json", // 日志输出格式
        EncoderConfig: zap.EncoderConfig{
            TimeKey:        "time", // 时间字段名
            LevelKey:       "level", // 日志等级字段名
            NameKey:        "logger", // 日志名字段名
            CallerKey:      "caller", // 调用者字段名
            MessageKey:     "msg", // 消息字段名
            StacktraceKey:  "stacktrace", // 堆栈字段名
            LineEnding:     zapcore.DefaultLineEnding,
            EncodeLevel:    zapcore.LowercaseLevelEncoder,
            EncodeTime:     zapcore.ISO8601TimeEncoder,
            EncodeDuration: zapcore.SecondsDurationEncoder,
            EncodeCaller:   zapcore.ShortCallerEncoder,
        },
        OutputPaths:      []string{"stdout"}, // 日志输出路径
        ErrorOutputPaths: []string{"stderr"}, // 错误日志输出路径
    }

    // 通过 Config 构建 Logger
    logger, _ := config.Build()

    // 在程序结束时同步缓冲区
    defer logger.Sync()

    // 记录日志
    logger.Info("hello!", zap.String("name", "xiaomin"), zap.Int("age", 20))
}

记录调用信息

Zap 提供了 AddCaller() 方法,可以记录调用者的信息,包括文件名、函数名、行号。
前提是必须设置 CallerKey 字段,因此 NewExample() 不能输出调用者信息。

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

  logger.Info("hello world")
}

输出结果:

{"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"hello world"}

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

func Output(msg string, fields ...zap.Field) {
  zap.L().Info(msg, fields...)
}

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

  zap.ReplaceGlobals(logger)
  Output("hello world")
}

输出结果:

{"level":"info","ts":1587740501.5592482,"caller":"skip/main.go:15","msg":"hello world"}

记录堆栈信息

Zap 提供了 AddStacktrace() 方法,可以记录堆栈信息。
前提是必须设置 StacktraceKey 字段,因此 NewExample() 不能输出堆栈信息。

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()
}

zapcore.WarnLevel 传入 AddStacktrace(),之后 Warn 和 Error 级别的日志会输出堆栈,Debug 和 Info 则不会。

运行结果:

{"level":"warn","ts":1729783529.2137501,"caller":"ConfessionWallServer/main.go:13","msg":"hello world","stacktrace":"main.f2\n\tD:/07_Github/ConfessionWallServer/main.go:13\nmain.f1\n\tD:/07_Github/ConfessionWallServer/main.go:9\nmain.main\n\tD:/07_Github/ConfessionWallServer/main.go:22\nruntime.main\n\tC:/Program Files/Go/src/runtime/proc.go:272"}

stacktrace 单独拉出来看:

main.f2
D:/07_Github/ConfessionWallServer/main.go:13
    main.f1
    D:/07_Github/ConfessionWallServer/main.go:9
        main.main
        D:/07_Github/ConfessionWallServer/main.go:22
            runtime.main
            C:/Program Files/Go/src/runtime/proc.go:272

我们很清楚地看到调用路径。

预设日志字段

有些时候我们需要在每条日志中添加一些预设字段,可以通过 zap.Fields(fs ...Field) 来实现。

func main() {
  logger := zap.NewExample(zap.Fields(
    zap.Int("serverId", 114514),
    zap.String("serverName", "ConfessionWallServer"),
  ))

  logger.Info("hello world")
}

输出:

{"level":"info","msg":"hello world","serverId":114514,"serverName":"ConfessionWallServer"}

设置全局 Logger

为了方便使用,Zap 提供了 ReplaceGlobals(logger *Logger),可以将一个 Logger 设置为全局的 Logger。
zap.L() 获取全局 Logger,zap.S() 获取全局 Sugared Logger。

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  zap.ReplaceGlobals(logger)
  zap.L().Info("Global Logger")
  zap.S().Infof("Global %s", "Sugared Logger")
}

输出:

{"level":"info","msg":"Global Logger"}
{"level":"info","msg":"Global Sugared Logger"}

注意:若没有设置全局 Logger,则调用 zap.L()zap.S() 并不会有日志输出。

实现日志滚动

lumberjack 是一个高效且易用的日志滚动包,它允许开发人员将日志写入自动滚动的文件中,从而使日志管理更加简单。

go get -u github.com/natefinch/lumberjack

lumberjack 提供了一个滚动记录器 logger,它实现了 io.Writerio.Closer 接口,我们可以使用 zapcore.AddSync 来将其与 Zap 结合使用。

func main() {
    lumberJackLogger := &lumberjack.Logger{
        Filename:   filename,  // 文件路径
        MaxSize:    maxsize,   // 单个日志文件的最大大小(单位为MB)
        MaxAge:     maxAge,    // 保留旧文件的最大天数
        MaxBackups: maxBackup, // 保留旧文件的最大个数
        Compress:   false,     // 是否压缩/归档旧文件
    }

    core := zapcore.NewCore(
        zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
        zapcore.AddSync(lumberJackLogger),
        zapcore.DebugLevel,
    )

    logger := zap.New(core)
}

更多高级用法请参考 Github 项目文档,在此不多赘述。


使用 Zap 实现高性能日志记录
https://blog.sugarmgp.icu/2024/10/24/zap/
作者
SugarMGP
发布于
2024年10月24日
许可协议