FunTester Zap 高性能日志库实践

FunTester · 2024年06月11日 · 1859 次阅读

Zap 是一个由 Uber 公司开源的结构化、高性能日志记录库,旨在为 Go 语言提供一种快速、简单且高效的日志解决方案。它起源于 Uber 内部使用的日志系统,后来于 2016 年开源,迅速获得了 Go 社区的广泛关注和应用。

Zap 的主要特点如下:

  1. 高性能:Zap 在设计时就非常注重性能,比标准库 log 包快几个数量级,即使在高并发场景下也能保持出色的性能表现。
  2. 结构化日志:Zap 支持结构化日志记录,可以方便地记录任意类型的字段,而不仅限于字符串,这有利于后期日志分析和处理。
  3. 级别控制:Zap 提供了丰富的日志级别控制,可以动态修改日志级别,从而只输出关键日志或调试日志。
  4. 编码支持:Zap 内置支持 JSON 和控制台的日志编码,并提供了钩子机制来扩展其他编码格式。
  5. 日志分割:Zap 支持根据日期、大小等条件自动分割日志文件,方便日志文件管理和分析。

Zap 广泛应用于各种 Go 项目中,尤其是那些对性能、日志结构化和可观测性有较高要求的场景,如微服务、分布式系统等。很多知名的 Go 项目和公司都在使用 Zap,例如 Kubernetes、Istio、InfluxData 等。通过 Zap,开发者可以获得高效、灵活且易于管理的日志解决方案,从而更好地监控和调试应用程序。

下面我们来进行 zap 日志库的上手实践。

依赖

我个人比较习惯配置在 go.mod 文件当中,但是搜索了几页居然都没有发现,只好采用了官方给的命令安装依赖方式:

go get -u go.uber.org/zap

然后我发现了 go.mod 文件已经有了相对应的配置,如下:

go.uber.org/zap v1.27.0 // indirect

在后面实践当中还会用到其他的依赖,这里一起发一下配置:

github.com/natefinch/lumberjack v2.0.0+incompatible // indirect  
go.uber.org/multierr v1.11.0 // indirect  
go.uber.org/zap v1.27.0 // indirect

小试牛刀

下面我们先来一个基础的 Case 来熟悉一下 zap 日志库的的使用语法:

//  
// TestLogZap  
//  @Description: 测试zap日志  
//  @param t  
//  
func TestLogZap(t *testing.T) {  
    logger, _ := zap.NewProduction() // 创建一个新的 Logger 实例  
    defer logger.Sync()              // 确保缓冲区中的日志条目被刷新  
    logger.Info("FunTester,例子",      // 使用 logger 记录日志  
       zap.String("name", "FunTester"), // 结构化上下文  
       zap.Int("score", 100),           // 结构化上下文  
    )  
    logger.Info("warn FunTester coming!!!")   // 使用 logger 记录警告日志  
    logger.Warn("warn FunTester coming!!!")   // 使用 logger 记录警告日志  
    logger.Error("error FunTester coming!!!") // 使用 logger 记录错误日志  
}

下面是控制台输出:

=== RUN   TestLogZap
{"level":"info","ts":1717310460.23924,"caller":"test/zap_test.go:16","msg":"This is an info message","category":"example","counter":1}
{"level":"info","ts":1717310460.2393,"caller":"test/zap_test.go:20","msg":"warn FunTester coming!!!"}
{"level":"warn","ts":1717310460.2393029,"caller":"test/zap_test.go:21","msg":"warn FunTester coming!!!"}
{"level":"error","ts":1717310460.239305,"caller":"test/zap_test.go:22","msg":"error FunTester coming!!!","stacktrace":"funtester/test.TestLogZap\n\t/Users/oker/GolandProjects/funtester/test/zap_test.go:22\ntesting.tRunner\n\t/opt/homebrew/opt/go/libexec/src/testing/testing.go:1689"}
--- PASS: TestLogZap (0.00s)
PASS

可以看到,这里的输出格式均是 JSON 格式的日志信息,对于不同的级别,输出的日志信息中,都包含了 caller 信息,但是 error 日志多了一个 stacktrace 信息。

这里是我查到的 zap 默认的配置信息:

Debug 级别日志:包含调用者信息,但不包含堆栈信息。
Info 级别日志:包含调用者信息,但不包含堆栈信息。
Warn 级别日志:包含调用者信息,但不包含堆栈信息。
Error 级别日志:包含调用者信息,并包含堆栈信息。
DPanic 级别日志:包含调用者信息,并包含堆栈信息。
Panic 级别日志:包含调用者信息,并包含堆栈信息。
Fatal 级别日志:包含调用者信息,并包含堆栈信息。

sugar

在 zap 日志库中,除了提供高性能、结构化的日志记录功能外,还提供了一个简化的日志记录接口,称为 “Sugared Logger”。Sugared Logger 提供了一种更简便的方式来记录日志,适合那些不需要严格结构化日志的场景。

Sugared Logger(糖化日志记录器)是一种在使用上更灵活、语法更简洁的日志记录器。与 zap 的原生结构化日志记录器相比,Sugared Logger 提供了类似于 fmt.Printf 风格的方法,这使得记录日志更为简便,但在性能上略有损失。

下面是一个使用的例子:

func TestLogZapSugar(t *testing.T) {  
    logger, _ := zap.NewProduction() // 创建一个新的 Logger 实例  
    defer logger.Sync()              // 确保缓冲区中的日志条目被刷新  
    sugar := logger.Sugar()          // 使用 Sugar 方法创建一个新的 Logger 实例  
    sugar.Infow("调用失败",              // 使用 Sugar 方法记录日志  
       "方法", "FunTester",  
       "调用次数", 3,  
       "时间单位", time.Second,  
    )  
    sugar.Infof("调用方法失败 %s", "FunTester") // 使用 Sugar 方法记录日志  
}

下面是日志输出:

=== RUN   TestLogLevel
2024-06-02T14:57:28.298+0800    INFO    test/zap_test.go:62 This is a custom logger info message    {"category": "custom", "counter": 1}
2024-06-02T14:57:28.299+0800    WARN    test/zap_test.go:66 This is a custom logger warning message
2024-06-02T14:57:28.299+0800    ERROR   test/zap_test.go:67 This is a custom logger error message
2024-06-02T14:57:28.299+0800    INFO    test/zap_test.go:68 This is a structured log message    {"key1": "value1", "key2": 42}
--- PASS: TestLogLevel (0.00s)
PASS

这样看起来是不是就更加如何常见的日志格式了条例清理,不同的信息按列显示。

日志等级

下面我们来演示一下如何更加精细化使用日志等级,将超过某个等级的日志输出到控制台上。代码如下:

func TestLogLevel(t *testing.T) {  
    encoderConfig := zapcore.EncoderConfig{ // 创建编码配置  
       TimeKey:        "T",                           // 时间键  
       LevelKey:       "L",                           // 日志级别键  
       NameKey:        "log",                         // 日志名称键  
       CallerKey:      "C",                           // 日志调用键  
       MessageKey:     "msg",                         // 日志消息键  
       StacktraceKey:  "stacktrace",                  // 堆栈跟踪键  
       LineEnding:     zapcore.DefaultLineEnding,     // 行结束符,默认为 \n       EncodeLevel:    zapcore.CapitalLevelEncoder,   // 日志级别编码器,将日志级别转换为大写  
       EncodeTime:     zapcore.ISO8601TimeEncoder,    // 时间编码器,将时间格式化为 ISO8601 格式  
       EncodeDuration: zapcore.StringDurationEncoder, // 持续时间编码器,将持续时间编码为字符串  
       EncodeCaller:   zapcore.ShortCallerEncoder,    // 调用编码器,显示文件名和行号  
    }  
    encoder := zapcore.NewConsoleEncoder(encoderConfig)                    // 创建控制台编码器,使用编码配置  
    atomicLevel := zap.NewAtomicLevel()                                    // 创建原子级别,用于动态设置日志级别  
    atomicLevel.SetLevel(zap.InfoLevel)                                    // 设置日志级别,只有 Info 级别及以上的日志才会输出  
    core := zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), atomicLevel) // 将日志输出到标准输出  
    logger := zap.New(core, zap.AddCaller(), zap.Development())            // 创建 Logger,添加调用者和开发模式  
    defer logger.Sync()  
    logger.Warn("打印警告日志")  
    logger.Error("打印错误日志")  
    logger.Info("打印结构化日志",  
       zap.String("key1", "FunTester"),  
       zap.Int("key2", 22),  
    )  
}

控制台输出如下:

=== RUN   TestLogLevel
2024-06-02T15:29:40.686+0800    WARN    test/zap_test.go:61 打印警告日志
2024-06-02T15:29:40.687+0800    ERROR   test/zap_test.go:62 打印错误日志
2024-06-02T15:29:40.687+0800    INFO    test/zap_test.go:63 打印结构化日志   {"key1": "FunTester", "key2": 22}
--- PASS: TestLogLevel (0.00s)
PASS

可以看到,info 以上的日志输出到控制台了。

日志文件

之前我们案例中都没有设置将日志输出到文件,下面我们来学习将日志输入到日志文件中的应用。

func TestLogFile(t *testing.T) {  
    logDir := "logs"                                  // 日志目录,不存在则创建  
    if err := os.MkdirAll(logDir, 0755); err != nil { // 创建日志目录  
       panic(err)  
    }  
    logFile := filepath.Join(logDir, "app.log")                                  // 日志文件,不存在则创建  
    file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // 创建日志文件  
    if err != nil {  
       panic(err)  
    }  
    encoderConfig := zapcore.EncoderConfig{ // 创建编码配置  
       TimeKey:        "T",                           // 时间键  
       LevelKey:       "L",                           // 日志级别键  
       NameKey:        "log",                         // 日志名称键  
       CallerKey:      "C",                           // 日志调用键  
       MessageKey:     "msg",                         // 日志消息键  
       StacktraceKey:  "stacktrace",                  // 堆栈跟踪键  
       LineEnding:     zapcore.DefaultLineEnding,     // 行结束符,默认为 \n       EncodeLevel:    zapcore.CapitalLevelEncoder,   // 日志级别编码器,将日志级别转换为大写  
       EncodeTime:     zapcore.ISO8601TimeEncoder,    // 时间编码器,将时间格式化为 ISO8601 格式  
       EncodeDuration: zapcore.StringDurationEncoder, // 持续时间编码器,将持续时间编码为字符串  
       EncodeCaller:   zapcore.ShortCallerEncoder,    // 调用编码器,显示文件名和行号  
    }  
    encoder := zapcore.NewJSONEncoder(encoderConfig)           // 创建 JSON 编码器  
    consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig) // 创建控制台编码器  
    writeSyncer := zapcore.AddSync(file)                       // 创建 WriteSyncer    consoleWriteSyncer := zapcore.AddSync(os.Stdout)           // 创建控制台 WriteSyncer    atomicLevel := zap.NewAtomicLevel()                        // 创建原子级别  
    atomicLevel.SetLevel(zap.InfoLevel)                        // 设置日志级别  
    core := zapcore.NewCore(encoder, writeSyncer, atomicLevel) // 创建 Core,将日志输出到文件  
    consoleCore := zapcore.NewCore(consoleEncoder, consoleWriteSyncer, atomicLevel)  
    combinedCore := zapcore.NewTee(core, consoleCore)                   // 创建多个 Core,将日志同时输出到文件和控制台  
    logger := zap.New(combinedCore, zap.AddCaller(), zap.Development()) // 创建 Logger,添加调用者和开发模式  
    defer logger.Sync()                                                 // 确保缓冲区中的日志条目被刷新  
    logger.Warn("打印警告日志")  
    logger.Error("打印错误日志")  
    logger.Info("打印结构化日志",  
       zap.String("key1", "FunTester"),  
       zap.Int("key2", 22),  
    )  
}

控制台输出:

=== RUN   TestLogFile
2024-06-02T15:40:30.260+0800    WARN    test/zap_test.go:103    打印警告日志
2024-06-02T15:40:30.261+0800    ERROR   test/zap_test.go:104    打印错误日志
2024-06-02T15:40:30.261+0800    INFO    test/zap_test.go:105    打印结构化日志   {"key1": "FunTester", "key2": 22}
--- PASS: TestLogFile (0.01s)
PASS

日志文件内容:

{"L":"WARN","T":"2024-06-02T15:40:30.260+0800","C":"test/zap_test.go:103","msg":"打印警告日志"}  
{"L":"ERROR","T":"2024-06-02T15:40:30.261+0800","C":"test/zap_test.go:104","msg":"打印错误日志"}  
{"L":"INFO","T":"2024-06-02T15:40:30.261+0800","C":"test/zap_test.go:105","msg":"打印结构化日志","key1":"FunTester","key2":22}

日志分割

在实际的项目当中,我们通常会对日志进行分割(比如按大小分割),下面我们来演示一下使用 zap 框架时,进行日志分割的例子。

func TestLogFileLumberjack(t *testing.T) {  
    writeSyncer := zapcore.AddSync(&lumberjack.Logger{ // 创建 WriteSyncer,使用 lumberjack.Logger,支持日志切割  
       Filename:   "logs/app.log",  
       MaxSize:    10,   // 每个日志文件最大 10 MB       MaxBackups: 5,    // 保留最近的 5 个日志文件  
       MaxAge:     30,   // 保留最近 30 天的日志  
       Compress:   true, // 旧日志文件压缩  
    })  
    encoderConfig := zapcore.EncoderConfig{ // 创建编码配置  
       TimeKey:        "T",                           // 时间键  
       LevelKey:       "L",                           // 日志级别键  
       NameKey:        "log",                         // 日志名称键  
       CallerKey:      "C",                           // 日志调用键  
       MessageKey:     "msg",                         // 日志消息键  
       StacktraceKey:  "stacktrace",                  // 堆栈跟踪键  
       LineEnding:     zapcore.DefaultLineEnding,     // 行结束符,默认为 \n       EncodeLevel:    zapcore.CapitalLevelEncoder,   // 日志级别编码器,将日志级别转换为大写  
       EncodeTime:     zapcore.ISO8601TimeEncoder,    // 时间编码器,将时间格式化为 ISO8601 格式  
       EncodeDuration: zapcore.StringDurationEncoder, // 持续时间编码器,将持续时间编码为字符串  
       EncodeCaller:   zapcore.ShortCallerEncoder,    // 调用编码器,显示文件名和行号  
    }  
    encoder := zapcore.NewJSONEncoder(encoderConfig)            // 创建 JSON 编码器  
    atomicLevel := zap.NewAtomicLevel()                         // 创建原子级别  
    atomicLevel.SetLevel(zap.InfoLevel)                         // 设置日志级别  
    core := zapcore.NewCore(encoder, writeSyncer, atomicLevel)  // 创建 Core,将日志输出到文件  
    logger := zap.New(core, zap.AddCaller(), zap.Development()) // 创建 Logger,添加调用者和开发模式  
    defer logger.Sync()                                         // 确保缓冲区中的日志条目被刷新  
    logger.Warn("打印警告日志")  
    logger.Error("打印错误日志")  
    logger.Info("打印结构化日志",  
       zap.String("key1", "FunTester"),  
       zap.Int("key2", 22),  
    )  
}

控制台日志打印和文件分割效果这里就不展示了。各位有兴趣可以自测一波。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册