写点什么

Zap 高性能日志库实践

作者:FunTester
  • 2024-06-11
    河北
  • 本文字数:6371 字

    阅读完需:约 21 分钟

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   TestLogLevel2024-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 message2024-06-02T14:57:28.299+0800  ERROR  test/zap_test.go:67  This is a custom logger error message2024-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   TestLogLevel2024-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   TestLogFile2024-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),      )  }
复制代码


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

发布于: 刚刚阅读数: 3
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Zap高性能日志库实践_FunTester_InfoQ写作社区