• 34-zap日志库


    在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能:

    • 能够将事件记录到文件中,而不是应用程序控制台。
    • 日志切割-能够根据文件大小、时间或间隔等来切割日志文件。
    • 支持不同的日志级别。例如INFO,DEBUG,ERROR等。
    • 能够打印基本信息,如调用文件/函数名和行号,日志时间等。

    默认的GO Logger

    Go语言提供的默认日志包是:https://golang.org/pkg/log/。

    实现Go Logger

    设置Logger

    我们可以像下面代码一样设置日志记录器

    func SetupLogger(){
      logFileLocation,_ := os.OpenFile("/Users/alblue/test.log",os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
      log.SetOutput(logFileLocation)
    }
    
    • 1
    • 2
    • 3
    • 4
    使用Logger

    让我们来写一些虚拟的代码来使用这个日志记录器。

    在当前的示例中,我们将建立一个到URL的HTTP的连接,并将状态代码/错误记录到日志文件中。

    func simpleHttpGet(url string) {
      resp, err := http.Get(url)
      if err != nil {
        log.Prinf("Error fetching url %s : %s",url, err.Error())
      } else {
        log.Printf("status code for %s : %s", url, resp.Status)
        resp.Body.close()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    Logger的运行

    现在让我们执行上面的代码并查看日志记录器的运行情况。

    func main(){
      SetupLogger()
      simpleHttpGet("www.google.com")
      simpleHttpGet("http://www.google.com")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当我们执行上面的代码,我们能看到一个test.log文件被创建,下面的内容会被添加到这个日志文件中。

    2022/05/24 01:14:13 Error fetching url www.google.com : Get www.google.com: unsupported protocol scheme ""
    2022/05/24 01:14:14 Status Code for http://www.google.com : 200 OK
    
    • 1
    • 2

    Go Logger的优势和劣势

    优势

    最大的优点就是使用非常简单。我们可以设置任何的io.Writer作为日志记录输出并向其发送要写入的日志。

    劣势

    • 仅限基本的日志级别

      • 只有一个Print选项。不支持INFO/DEBUG等多个级别。
    • 对于错误日志,它有FatalPanic

      • Fatal日志通过调用os.Exit(1)来结束程序
      • Panic日志在写入日志消息之后抛出一个panic
      • 但是它缺少一个ERROR日志级别,这个级别可以在不抛出panic或退出程序的情况下记录错误
    • 缺乏日志格式化的能力——例如记录调用者的函数名和行号,格式化日期和时间格式。等等。

    • 不提供日志切割的能力。

    Uber-go Zap

    为什么选择Uber-go zap

    • 它同时提供了结构化日志记录和printf风格的日志记录。
    • 它非常的快。

    根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好——也比标准库更快。 以下是Zap发布的基准测试信息

    记录一条消息和10个字段:

    PackageTimeTime % to zapObjects Allocated
    ⚡️ zap862 ns/op+0%5 allocs/op
    ⚡️ zap (sugared)1250 ns/op+45%11 allocs/op
    zerolog4021 ns/op+366%76 allocs/op
    go-kit4542 ns/op+427%105 allocs/op
    apex/log26785 ns/op+3007%115 allocs/op
    logrus29501 ns/op+3322%125 allocs/op
    log1529906 ns/op+3369%122 allocs/op

    记录一个静态字符串,没有任何上下文或printf风格的模板:

    PackageTimeTime % to zapObjects Allocated
    ⚡️ zap118 ns/op+0%0 allocs/op
    ⚡️ zap (sugared)191 ns/op+62%2 allocs/op
    zerolog93 ns/op-21%0 allocs/op
    go-kit280 ns/op+137%11 allocs/op
    standard library499 ns/op+323%2 allocs/op
    apex/log1990 ns/op+1586%10 allocs/op
    logrus3129 ns/op+2552%24 allocs/op
    log153887 ns/op+3194%23 allocs/op

    安装

    运行下面的命令安装zap

    go get -u go.uber.org/zap
    
    • 1

    配置Zap Logger

    Zap提供了两种类型的日志记录器- Sugared LoggerLogger

    在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10。并且支持结构化和printf风格的日志记录。

    在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。

    工作中一般使用Logger就行。

    Logger 基本使用
    • 通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger。
    • 上面的每一个函数都将创建一个logger。唯一区别在于它将记录的信息不同。例如production logger默认记录调用函数信息,日期和时间等。
    • 通过Logger调用Info/Error等。
    • 默认情况下日志都会打印到应用程序的console界面。
    func zapDemo1() {
    	// 获取 logger对象
    	logger, err := zap.NewProduction()
    	if err != nil {
    		panic(err)
    	}
    
    	// 记录日志
    	var uid int64 = 123456
    	isLogin := true
    	name := "爱写代码的小男孩"
    	data := []int{1, 2}
    
    	// 日志输出:默认是输出JSON格式,日志会输出到标准输出(终端)
    	logger.Info("欢迎关注爱写代码的小男孩", zap.Int64("uid", uid),
    		zap.Bool("isLogin", isLogin),
    		zap.String("name", name),
    		// zap.Any("data",data) // any表示可以不区分类型
    		zap.Ints("data", data))
    }
    
    // 结果输出为json格式
    {"level":"info","ts":1652091983.2841868,"caller":"zap_demo/main.go:19","msg":"欢迎关注爱写代码的小男孩","uid":123456,"isLogin":true,"name":"爱写代码的小男孩","data":[1,2]}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    定制logger
    定制一:将日志写入文件而不是终端

    我们要做的第一个更改是把日志写入文件,而不是打印到应用程序控制台。

    • 我们将使用zap.New(…)方法来手动传递所有配置,而不是使用像zap.NewProduction()这样的预置方法来创建logger。

      func New(core zapcore.Core,options ...Option) *Logger
      
      • 1

      zapcore.Core需要三个配置-----Encoder,WriteSyncer,LogLevel

    1、Encoder:编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig()

    zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
    
    • 1

    2、WriterSyncer:指定日志将写到哪里去。我们使用zapcore.AddSync()函数并且将打开的文件句柄传进去。

    file , _ :=os.Create("./test.log")
    writerSyncer := zapcore.AddSync(file)
    
    • 1
    • 2

    3、Log Level:哪种级别的日志将被写入。

    开发阶段会记录很多debug级别的日志方便调试。

    实际生产环境不需要记录那些debug日志,所以可以将最终生产环境的日志级别定位info,只有大于等于info级别的日志才会记录。
    在这里插入图片描述

    // 实际记录日志的时候 log.check 就是针对日志级别做检查,只有满足条件的日志才ce.Write(fields...)
    func (log *Logger) Info(msg string, fields ...Field) {
    	if ce := log.check(InfoLevel, msg); ce != nil {
    		ce.Write(fields...)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代码如下:

    // 将日志输出到文件中,而不是输出到终端
    func zapDemo2() {
    	// 1、 encoder编码
    	encoderCfg := zap.NewProductionEncoderConfig()
    	encoder := zapcore.NewJSONEncoder(encoderCfg) // 定义日志格式以json格式
    
    	// 2、writerSyncer 将日志写到文件
    	file, _ := os.OpenFile("./app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0664)
    	fileWS := zapcore.AddSync(file)
    
    	// 3、设置log level
    	level := zapcore.DebugLevel
    	// level, err := zapcore.ParseLevel(s) // 假设从外面读入s的值:s="info"
    
    	// 创建zapcore
    	core := zapcore.NewCore(encoder, fileWS, level)
    	// 通过zapcore创建logger
    	logger := zap.New(core)
    
    	// 测试
    	logger.Debug("这是一条debug的日志")
    	logger.Info("这是一条info的日志")
    	logger.Warn("这是一条warning的日志")
    	logger.Error("这是一条error的日志")
    
    }
    
    func zapDemo1() {
    	// 获取 logger对象
    	logger, err := zap.NewProduction()
    	if err != nil {
    		panic(err)
    	}
    
    	// 记录日志
    	var uid int64 = 123456
    	isLogin := true
    	name := "爱写代码的小男孩"
    	data := []int{1, 2}
    
    	// 日志输出:默认是输出JSON格式,日志会输出到标准输出(终端)
    	logger.Info("欢迎关注爱写代码的小男孩", zap.Int64("uid", uid),
    		zap.Bool("isLogin", isLogin),
    		zap.String("name", name),
    		// zap.Any("data",data) // any表示可以不区分类型
    		zap.Ints("data", data))
    }
    
    // 运行结果
    出现一个app.log文件
    {"level":"debug","ts":1652094274.631673,"msg":"这是一条debug的日志"}
    {"level":"info","ts":1652094274.631768,"msg":"这是一条info的日志"}
    {"level":"warn","ts":1652094274.631781,"msg":"这是一条warning的日志"}
    {"level":"error","ts":1652094274.631791,"msg":"这是一条error的日志"}
    
    • 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
    定制二:将日志写入文件和终端

    代码如下:

    // 将日志既输出终端又输出到文件中
    func zapDemo3() {
    	// 1、encoder编码
    	encoderCfg := zap.NewProductionEncoderConfig()
    	encoder := zapcore.NewJSONEncoder(encoderCfg)
    
    	// 2、writerSyncer 将日志写到文件和终端
    	file, _ := os.OpenFile("./app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    	fileWS := zapcore.AddSync(file)
    	consoleWS := zapcore.AddSync(os.Stdout)
    
    	// 3、 设置loglevel
    	level := zapcore.DebugLevel
    
    	// 创建zapcore
    	core := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(fileWS, consoleWS), level) // NewMultiWriteSyncer:可以同时多个终端输出
    	// 创建logger
    	logger := zap.New(core)
    
    	// 测试
    	logger.Debug("这是测试即在终端,又在文件的debug日志")
    	logger.Info("这是测试即在终端,又在文件的info日志")
    	logger.Warn("这是测试即在终端,又在文件的warn日志")
    	logger.Error("这是测试即在终端,又在文件的error日志")
    
    }
    
    // 输出结果
    终端和文件均输出:
    {"level":"debug","ts":1652097063.1497128,"msg":"这是测试即在终端,又在文件的debug日志"}
    {"level":"info","ts":1652097063.1497989,"msg":"这是测试即在终端,又在文件的info日志"}
    {"level":"warn","ts":1652097063.149816,"msg":"这是测试即在终端,又在文件的warn日志"}
    {"level":"error","ts":1652097063.1498282,"msg":"这是测试即在终端,又在文件的error日志"}
    
    • 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
    定制三:将error级别的日志单独记录到一个日志文件中,比如:app-err.log

    代码如下:

    // 将error级别的日志写在一个文件中。
    func zapDemo4() {
    	// 1、encoder
    	encodercfg := zap.NewProductionEncoderConfig()
    	encoder := zapcore.NewJSONEncoder(encodercfg)
    
    	// 2、writerSyncer
    	file1, _ := os.Create("./app.log")
    	file2, _ := os.Create("./app-err.log")
    
    	core1 := zapcore.NewCore(encoder, zapcore.AddSync(file1), zapcore.DebugLevel)
    	core2 := zapcore.NewCore(encoder, zapcore.AddSync(file2), zapcore.ErrorLevel)
    
    	// 将两个core合并成一个新的core
    	newcore := zapcore.NewTee(core1, core2)
    	// 创建logger
    	logger := zap.New(newcore)
    
    	// 测试
    	logger.Debug("这是一条debug的日志")
    	logger.Info("这是一条info的日志")
    	logger.Warn("这是一条warning的日志")
    	logger.Error("这是一条error的日志")
    
    }
    
    
    // 运行结果
    app.log日志内容:
    {"level":"debug","ts":1652098034.071912,"msg":"这是一条debug的日志"}
    {"level":"info","ts":1652098034.07199,"msg":"这是一条info的日志"}
    {"level":"warn","ts":1652098034.0720031,"msg":"这是一条warning的日志"}
    {"level":"error","ts":1652098034.072012,"msg":"这是一条error的日志"}
    app-err.log日志内容
    {"level":"error","ts":1652098034.072012,"msg":"这是一条error的日志"}
    
    • 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
    定制四:将JSON Encoder更改为普通的Log Encoder

    现在,我们希望将编码器从JSON Encoder更改为普通的Encoder。为此,我们需要将NewJSONEncoder()更改为NewConsoleEncoder()。是以普通文本形式的。具体代码如下:

    // 更改JSON Encoder为普通的encoder
    func zapDemo5() {
    	// 1、encoder
    	encodingcfg := zap.NewProductionEncoderConfig()
    	encoder := zapcore.NewConsoleEncoder(encodingcfg) // 替换为普通的encoder
    
    	// 生成core
    	core := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel)
    
    	// 生成logger
    	logger := zap.New(core)
    
    	//测试
    	logger.Debug("这是一条debug的日志")
    	logger.Info("这是一条info的日志")
    	logger.Warn("这是一条warning的日志")
    	logger.Error("这是一条error的日志")
    }
    
    
    // 输出结果
    1.6520986319839091e+09  debug   这是一条debug的日志
    1.652098631983954e+09   info    这是一条info的日志
    1.652098631983958e+09   warn    这是一条warning的日志
    1.652098631983961e+09   error   这是一条error的日志
    
    • 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
    定制五:更改时间编码并添加添加调用者详细信息

    鉴于我们对配置所做的更改,有下面的两个问题:

    • 时间是以非人类可读的方式展示,例如1.572161051846623e+09
    • 调用方函数的详细信息没有显示在日志中

    我们要做的第一件事情就是覆盖默认的productionConfig(),并进行以下更改:

    • 修改时间编码器
    • 在日志文件中使用大写字母记录日志级别
    func getEncoder() zapcore.Encoder {
    	encoderConfig := zap.NewProductionEncoderConfig()
    	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
    	return zapcore.NewConsoleEncoder(encoderConfig)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接下来,我们将修改zap logger代码,添加将调用函数信息记录到日志中的功能。为此,我们将在zap.New(...)函数中添加一个Option

    logger := zap.New(core,zap.AddCaller())
    
    • 1

    完整代码如下:

    // 更改时间格式以及添加调用方法
    
    func zapDemo6() {
    	// 1、encoder
    	encodingcfg := zap.NewProductionEncoderConfig()
    	encodingcfg.TimeKey = "time"                          // 改变time的key
    	encodingcfg.EncodeTime = zapcore.ISO8601TimeEncoder   // 更改时间格式
    	encodingcfg.EncodeLevel = zapcore.CapitalLevelEncoder //将日志级别大写
    	enconder := zapcore.NewJSONEncoder(encodingcfg)
    
    	// 生成core
    	core := zapcore.NewCore(enconder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel)
    
    	//生成logger
    	logger := zap.New(core, zap.AddCaller())
    
    	// 测试
    	logger.Debug("这是一条debug的日志")
    	logger.Info("这是一条info的日志")
    	logger.Warn("这是一条warning的日志")
    	logger.Error("这是一条error的日志")
    }
    
    
    
    // 输出结果
    {"level":"DEBUG","time":"2022-05-09T20:33:06.257+0800","caller":"zap_demo/main.go:27","msg":"这是一条debug的日志"}
    {"level":"INFO","time":"2022-05-09T20:33:06.257+0800","caller":"zap_demo/main.go:28","msg":"这是一条info的日志"}
    {"level":"WARN","time":"2022-05-09T20:33:06.257+0800","caller":"zap_demo/main.go:29","msg":"这是一条warning的日志"}
    {"level":"ERROR","time":"2022-05-09T20:33:06.257+0800","caller":"zap_demo/main.go:30","msg":"这是一条error的日志"}
    
    • 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

    日志切割

    Zap本身不支持切割归档文件,为了添加日志切割归档功能,将使用第三方库Lumberjack来实现。

    通常在公司里面一个小时切割一次日志文件,找起来方便,文件本身也不会太大

    一般日志文件大小不要超过500M

    zap本身是没有日志切割功能,

    日志切割可以使用系统工具:logrotate,也可以写脚本

    zap日志库有第三方的插件能够实现日志切割

    安装

    go get -u github.com/natefinch/lumberjack
    
    • 1

    Zap logger中加入Lumberjack

    要在zap中加入lumberjack支持,我们需要修改WriteSyncser代码。我们将按照下面的代码修改:

    lumberJackLogger := &lumberjack.Logger{
    		Filename:   "./test.log",
    		MaxSize:    10,
    		MaxBackups: 5,
    		MaxAge:     30,
    		Compress:   false,
    	}
    zapcore.AddSync(lumberJackLogger)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Lumberjack Logger采用以下属性作为输入:

    • Filename: 日志文件的位置
    • MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
    • MaxBackups:保留旧文件的最大个数
    • MaxAges:保留旧文件的最大天数
    • Compress:是否压缩/归档旧文件

    总结

    zap对象

    1. 自定义编码类型(普通文本、JSON)
      1. 字段的key
      2. 时间格式
      3. 日志级别的大小写
    2. 输出位置(文件、终端、多个文件)
    3. 日志级别
      1. 自定义基于日志级别的策略,比如忽略掉warn级别的日志(有兴趣的同学可以看一下)

    两种比较特殊的配置场景:

    1. 同时输出日志到文件和终端
    2. 将全量日志输出到 xx.log,同时将err级别的日志输出到xx.err.log

    logger 用法

    1. 全局的logger对象
    2. zap.L()
  • 相关阅读:
    无缝集结:一键式部署前后端分离项目,快速构建强大的容器化应用
    python基础(二、基础语法)
    数字电路常用芯片合集
    粒子滤波PF—从贝叶斯滤波到粒子滤波PF——Part-I(贝叶斯滤波)
    ONLYOFFICE8.1版本桌面编辑器测评
    【服务器数据恢复】HP StorageWorks系列服务器RAID5两块盘离线的数据恢复
    springsecurity实现单点登录
    【Unity实战篇】| 快速制作一个简易时钟,包括2D和3D时钟
    springboot的@ConditionalOnBean注解
    《QDebug 2022年6月》
  • 原文地址:https://blog.csdn.net/weixin_38753143/article/details/126227746