Golang zap日志库使用

2,170 阅读9分钟

前言

项目在开发阶段,经常会遇到各种问题。而想要快速的定位问题的位置,了解引发问题的原因,查看日志是相当有效的手段。那么我们需要什么样的日志呢?一个好的日志往往具备以下几个条件:

  • 能打印最基本的信息,例如调用的文件、函数名称、错误代码行号、记录时间等
  • 支持不同的日志级别,我们经常将级别划分为:InfoDebugErrorWarnFatal等不同程度
  • 能够持久化存储日志,例如将日志记录在文件中,并且能够根据需要来切割日志

而zap就完全满足了上面所述的几个条件,并且他非常高效,是结构化的、分级的go日志库。下面是zap发布的基准测试记录信息所消耗的时间对比:

image.png

image.png

1.1 zap的安装

直接使用 go get 拉取即可

go get -u go.uber.org/zap

1.2 创建实例

zap提供了两种类型的日志记录器SugaredLoggerLogger。在性能很好但不是很关键的上下文中,使用SugaredLogger,他比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录

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

注: 默认情况下日志都会打印到应用程序的终端界面

1.2.1 Logger

可以通过zap.NewProduction()zap.NewDevelopment() 或者 zap.Example() 来创建一个Logger

这三个方法的区别在于他将记录的信息不同,参数只能是string类型

var log *zap.Logger    //定义一个全局的Logger

log = zap.NewExample()
log, _ = zap.NewDevelopment()
log, _ = zap.NewProduction()

log.Debug("This is a DEBUG message")
log.Info("This is an INFO message")
log.Fatal("This ia a fatal message")

//zap.NewExample() 输出
{"level":"debug","msg":"This is a DEBUG message"}
{"level":"info","msg":"This is an INFO message"}

//zap.NewDevelopment() 输出
2022-11-19T10:07:36.345+0800    DEBUG   li/main.go:12   This is a DEBUG message
2022-11-19T10:07:36.394+0800    INFO    li/main.go:13   This is an INFO message

//zap.NewProduction() 输出
{"level":"info","ts":1668823728.0574305,"caller":"li/main.go:13","msg":"This is an INFO message"}
{"level":"fatal","ts":1668823905.9411304,"caller":"li/main.go:14","msg":"This ia a fatal message","stacktrace":"main.main\n\tD:/goproject/src/hub-gin server/li/main.go:14\nruntime.main\n\tD:/go/src/runtime/proc.go:250"}

这里要注意一点:zap.NewProduction()它只能记录 InfoLevel 及以上的日志级别

三种创建方式的对比

NewExample NewProduction 使用的是json格式输出,NewDevelopment使用行的形式输出。其中值得关注的就NewProductionNewDevelopment创建的Logger

  • NewDevelopment 是以 空格分开 的形式展示
  • NewProduction 使用的是json格式 展示出来
1.2.2 SugaredLogger

创建SugaredLogger有两种方式,方法如下:

var log *zap.Logger
var logger *zap.SugaredLogger   //第一种方式

func main() {
   //log = zap.NewExample()
   //log, _ = zap.NewDevelopment()
   //log, _ = zap.NewProduction()
   //log.Debug("This is a DEBUG message")
   //log.Info("This is an INFO message")
   //log.Fatal("This ia a fatal message")
   
   logger = log.Sugar()        //第二种方式
}

我们可以直接定义一个全局的SugaredLogger为后文使用,也可以调用Logger的.Sugar()方法获取一个SugaredLogger。支持printf格式记录日志

logger.Errorf("This is a Error message: Error = %s", err)

//{"level":"error","ts":1668825480.5430038,"caller":"li/main.go:21","msg":"This is a Error message: Error = xxxxxxxxx","stacktrace":"main.main\n\tD:/goproject/src/hub-gin-server/li/main.go:21\nruntime.main\n\tD:/go/src/runtime/proc.go:250"}

1.3 自定义logger -- 输出到文件

如果我们不仅想将日志打印在终端,还需要将日志记录到文件中进行持久化操作。那么我们可以进行自定义配置,使用zap.New()方法手动传递配置

// New constructs a new Logger from the provided zapcore.Core and Options. If
// the passed zapcore.Core is nil, it falls back to using a no-op
// implementation.
//
// This is the most flexible way to construct a Logger, but also the most
// verbose. For typical use cases, the highly-opinionated presets
// (NewProduction, NewDevelopment, and NewExample) or the Config struct are
// more convenient.
//
// For sample code, see the package-level AdvancedConfiguration example.
func New(core zapcore.Core, options ...Option) *Logger {
	if core == nil {
		return NewNop()
	}
	log := &Logger{
		core:        core,
		errorOutput: zapcore.Lock(os.Stderr),
		addStack:    zapcore.FatalLevel + 1,
		clock:       zapcore.DefaultClock,
	}
	return log.WithOptions(options...)
}

// Core is a minimal, fast logger interface. It's designed for library authors
// to wrap in a more user-friendly API.
type Core interface {
	LevelEnabler

	// With adds structured context to the Core.
	With([]Field) Core
	// Check determines whether the supplied Entry should be logged (using the
	// embedded LevelEnabler and possibly some extra logic). If the entry
	// should be logged, the Core adds itself to the CheckedEntry and returns
	// the result.
	//
	// Callers must use Check before calling Write.
	Check(Entry, *CheckedEntry) *CheckedEntry
	// Write serializes the Entry and any Fields supplied at the log site and
	// writes them to their destination.
	//
	// If called, Write should always log the Entry and Fields; it should not
	// replicate the logic of Check.
	Write(Entry, []Field) error
	// Sync flushes buffered logs (if any).
	Sync() error
}

通过源码我们可以看到New方法需要一个zapcore.Core参数,而Core是接口类型,那我们继续看如何实现Core这个接口,查看到源码中,如下:NewCore可以返回一个ioCore,然后再源码中ioCore实现了Core提供的几个接口

// NewCore creates a Core that writes logs to a WriteSyncer.
func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core {
	return &ioCore{
		LevelEnabler: enab,
		enc:          enc,
		out:          ws,
	}
}

type ioCore struct {
	LevelEnabler
	enc Encoder
	out WriteSyncer
}

我们可以看到只需要3个参数就能得到一个Logger那么这3个参数功能是什么呢?

  • Encoder

编码器(如何写入日志)。我们使用NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig()

// core 三个参数之  Encoder 编码
func getEncoder() zapcore.Encoder {
        //return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
        
	//自定义编码配置
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder   //修改时间编码器
        encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder //按级别显示不同颜色,不需要的话取值zapcore.CapitalLevelEncoder就可以了
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别

	//return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
	//return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	return zapcore.NewJSONEncoder(encoderConfig)
}
  • WriteSyncer

指定日志输出路径(文件 或 控制台 或者双向输出)。但是打开的类型不一样,文件打开的是io.writer类型,而我们需要的是WriteSyncer,所以我们使用zapcore.AddSync()函数进行转换

// core 三个参数之  日志输出路径
func getWriterSyncer() zapcore.WriteSyncer {
	file, _ := os.Create("./server/zaplog_test/log.log")
	return zapcore.AddSync(file)
}
  • LevelEnabler

设置打印的日志等级,通过它来动态的保存日志,比如上线后我们error以下的日志就不进行打印

我们通过 zapcore.***Level 来设置,里面都是封装好的日志等级:

const (
	// DebugLevel logs are typically voluminous, and are usually disabled in
	// production.
	DebugLevel Level = iota - 1
	// InfoLevel is the default logging priority.
	InfoLevel
	// WarnLevel logs are more important than Info, but don't need individual
	// human review.
	WarnLevel
	// ErrorLevel logs are high-priority. If an application is running smoothly,
	// it shouldn't generate any error-level logs.
	ErrorLevel
	// DPanicLevel logs are particularly important errors. In development the
	// logger panics after writing the message.
	DPanicLevel
	// PanicLevel logs a message, then panics.
	PanicLevel
	// FatalLevel logs a message, then calls os.Exit(1).
	FatalLevel

	_minLevel = DebugLevel
	_maxLevel = FatalLevel

	// InvalidLevel is an invalid value for Level.
	//
	// Core implementations may panic if they see messages of this level.
	InvalidLevel = _maxLevel + 1
)

通过配置上面的3个参数,我们就可以实现将日志输出到我们指定的文件中:

package main

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"os"
)

var logger *zap.Logger
var SugaredLogger *zap.SugaredLogger

func InitLogger() {
	encoder := getEncoder()
	writerSyncer := getWriterSyncer()
	core := zapcore.NewCore(encoder, writerSyncer, zap.DebugLevel)
	logger = zap.New(core)
	SugaredLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

func getWriterSyncer() zapcore.WriteSyncer {
	file, _ := os.Create("./server/zaplog_test/log.log")   //指定日志输出文件位置
	return zapcore.AddSync(file)
}

1.4 将日志同时输出到终端和文件

如果需要同时将日志输出到控制台终端和文件中,只需要改造一下zapcore.NewCore即可,本质其实就是修改一下 WriteSyncer ,使用zapcore.NewMultiWriteSyncer来设置多个输出对象

//还是以上文为例
func InitLogger() {
	encoder := getEncoder()
	writerSyncer := getWriterSyncer()
        
        //这里我们使用zapcore.NewMultiWriteSyncer()实现同时输出到多个对象中
	//multiWriteSyncer := zapcore.NewMultiWriteSyncer(writerSyncer, zapcore.AddSync(os.Stdout)) //AddSync将io.Writer转换成WriteSyncer的类型
	core := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(writerSyncer, zapcore.AddSync(os.Stdout)), zap.DebugLevel)
        
	logger = zap.New(core, zap.AddCaller()) //zap.AddCaller() 显示文件名 和 行号
	SugaredLogger = logger.Sugar()
}

1.5 实现日志文件切割

如果将所有的日志记录都存储到同一个文件中,这显然是不可取的,不仅会导致内存过大,也不利于我们翻阅日志排查错误,因此实现日志文件的合理切割是十分必要的。但是很可惜,zap并不支持切割日志文件,我们需要通过第三方库Lumberjack实现切割操作

import "gopkg.in/natefinch/lumberjack.v2"

要在zap中加入Lumberjack支持,我们需要修改WriteSyncer代码。我们将按照下面的代码修改getLogWriter()函数:

// core 三个参数之  日志输出路径
func getWriterSyncer() zapcore.WriteSyncer {
	//file, _ := os.Create("./server/zaplog_test/log.log")
	//return zapcore.AddSync(file)

	//引入第三方库 Lumberjack 加入日志切割功能
	lumberWriteSyncer := &lumberjack.Logger{
		Filename:   "./server/zaplog_test/log.log",
		MaxSize:    10, // megabytes
		MaxBackups: 100,
		MaxAge:     28,    // days
		Compress:   false, //Compress确定是否应该使用gzip压缩已旋转的日志文件。默认值是不执行压缩。
	}
	return zapcore.AddSync(lumberWriteSyncer)
}

这里我们设置的是 日志文件每 10MB 会切割并且在当前目录下最多保存 5 个日志文件,并且会将旧文档保存30天。

1.5.1 按照日志级别归档写入文件

为了管理人员的查询方便,一般我们需要将低于error级别的放到info.logerror及以上严重级别日志存放到error.log文件中,我们只需要改造一下zapcore.NewCore方法的第3个参数,然后将文件WriteSyncer拆成infoerror两个即可,也就是将上文的getWriterSyncer()拆分开:

/*
// core 三个参数之  日志输出路径
func getWriterSyncer() zapcore.WriteSyncer {
	//file, _ := os.Create("./server/zaplog_test/log.log")
	//return zapcore.AddSync(file)

	//引入第三方库 Lumberjack 加入日志切割功能
	lumberWriteSyncer := &lumberjack.Logger{
		Filename:   "./server/zaplog_test/log.log",
		MaxSize:    10, // megabytes
		MaxBackups: 100,
		MaxAge:     28,    // days
		Compress:   false, //Compress确定是否应该使用gzip压缩已旋转的日志文件。默认值是不执行压缩。
	}
	return zapcore.AddSync(lumberWriteSyncer)
}
*/

// 记录error以下日志级别的文件
func getInfoWriterSyncer() zapcore.WriteSyncer {
	//file, _ := os.Create("./server/zaplog/log.log")
	或者将上面的NewMultiWriteSyncer放到这里来,进行返回
	//return zapcore.AddSync(file)

	//引入第三方库 Lumberjack 加入日志切割功能
	infoLumberIO := &lumberjack.Logger{
		Filename:   "./server/zaplog/info.log",
		MaxSize:    10, // megabytes
		MaxBackups: 100,
		MaxAge:     28,    // days
		Compress:   false, //Compress确定是否应该使用gzip压缩已旋转的日志文件。默认值是不执行压缩。
	}
	return zapcore.AddSync(infoLumberIO)
}

//记录error及以上日志级别的文件
func getErrorWriterSyncer() zapcore.WriteSyncer {
	//引入第三方库 Lumberjack 加入日志切割功能
	lumberWriteSyncer := &lumberjack.Logger{
		Filename:   "./server/zaplog/error.log",
		MaxSize:    10, // megabytes
		MaxBackups: 100,
		MaxAge:     28,    // days
		Compress:   false, //Compress确定是否应该使用gzip压缩已旋转的日志文件。默认值是不执行压缩。
	}
	return zapcore.AddSync(lumberWriteSyncer)
}

在使用时只需要对日志级别进行判断,从而将不同级别的文件写入到不同的日志文件中

var logger *zap.Logger 
var SugaredLogger *zap.SugaredLogger

func InitLogger() {
	//获取编码器
	encoder := getEncoder()

	//对日志级别进行判断、分类
	highPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { //error级别
		return lev >= zap.ErrorLevel
	})
	lowPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { //info和debug级别,debug级别是最低的
		return lev < zap.ErrorLevel && lev >= zap.DebugLevel
	})

	//info文件WriteSyncer
	infoFileWriteSyncer := getInfoWriterSyncer()
	//error文件WriteSyncer
	errorFileWriteSyncer := getErrorWriterSyncer()

	//生成core
	//multiWriteSyncer := zapcore.NewMultiWriteSyncer(writerSyncer, zapcore.AddSync(os.Stdout)) //AddSync将io.Writer转换成WriteSyncer的类型
	//同时输出到控制台 和 指定的日志文件中
	infoFileCore := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(infoFileWriteSyncer, zapcore.AddSync(os.Stdout)), lowPriority)
	errorFileCore := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(errorFileWriteSyncer, zapcore.AddSync(os.Stdout)), highPriority)

	//将infocore 和 errcore 加入core切片
	var coreArr []zapcore.Core
	coreArr = append(coreArr, infoFileCore)
	coreArr = append(coreArr, errorFileCore)

	//生成logger
	logger = zap.New(zapcore.NewTee(coreArr...), zap.AddCaller()) //zap.AddCaller() 显示文件名 和 行号
	SugaredLogger = logger.Sugar()
}

这样修改之后,infodebug级别的日志就存放到info.logerror级别的日志单独放到error.log文件中了

结语

通过上文对zap的各种配置,我们就可以在项目中高效的记录日志、查阅日志了。如果有什么不足,欢迎大家斧正

文章参考: (17条消息) go zap日志库的使用,以及封装。_我在窗野望江湖的博客-CSDN博客

image.png