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)
}
使用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()
  }
}
Logger的运行

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

func main(){
  SetupLogger()
  simpleHttpGet("www.google.com")
  simpleHttpGet("http://www.google.com")
}

当我们执行上面的代码,我们能看到一个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
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
配置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]}
定制logger
定制一:将日志写入文件而不是终端

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

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

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

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

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

zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

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

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

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

代码如下:

// 将日志输出到文件中,而不是输出到终端
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的日志"}
定制二:将日志写入文件和终端

代码如下:

// 将日志既输出终端又输出到文件中
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日志"}
定制三:将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的日志"}
定制四:将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.572161051846623e+09
  • 调用方函数的详细信息没有显示在日志中

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

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

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

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

完整代码如下:

// 更改时间格式以及添加调用方法

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的日志"}

日志切割

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

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

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

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

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

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

安装
go get -u github.com/natefinch/lumberjack
Zap logger中加入Lumberjack

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

lumberJackLogger := &lumberjack.Logger{
		Filename:   "./test.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     30,
		Compress:   false,
	}
zapcore.AddSync(lumberJackLogger)

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()
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值