前言
项目在开发阶段,经常会遇到各种问题。而想要快速的定位问题的位置,了解引发问题的原因,查看日志是相当有效的手段。那么我们需要什么样的日志呢?一个好的日志往往具备以下几个条件:
- 能打印最基本的信息,例如调用的文件、函数名称、错误代码行号、记录时间等
- 支持不同的日志级别,我们经常将级别划分为:
Info、Debug、Error、Warn、Fatal等不同程度 - 能够持久化存储日志,例如将日志记录在文件中,并且能够根据需要来切割日志
而zap就完全满足了上面所述的几个条件,并且他非常高效,是结构化的、分级的go日志库。下面是zap发布的基准测试记录信息所消耗的时间对比:
1.1 zap的安装
直接使用 go get 拉取即可
go get -u go.uber.org/zap
1.2 创建实例
zap提供了两种类型的日志记录器SugaredLogger和Logger。在性能很好但不是很关键的上下文中,使用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使用行的形式输出。其中值得关注的就NewProduction、NewDevelopment创建的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.log,error及以上严重级别日志存放到error.log文件中,我们只需要改造一下zapcore.NewCore方法的第3个参数,然后将文件WriteSyncer拆成info和error两个即可,也就是将上文的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()
}
这样修改之后,info和debug级别的日志就存放到info.log,error级别的日志单独放到error.log文件中了
结语
通过上文对zap的各种配置,我们就可以在项目中高效的记录日志、查阅日志了。如果有什么不足,欢迎大家斧正
文章参考: (17条消息) go zap日志库的使用,以及封装。_我在窗野望江湖的博客-CSDN博客