1.介绍

Zapuber开源的日志库,支持日志级别分级 、结构化记录,对性能和内存分配做了极致的优化。目前 Star 12.8 源码地址: https://github.com/uber-go/zap

官方性能测试图

2.安装

go get -u go.uber.org/zap

3.日志记录器

Zap提供了两种类型的日志记录器: SugaredLoggerLogger,两者对比如下:

SugaredLogger : 在性能很好但不是很关键的上下文中使用,它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。与 log15go-kit 一样,SugaredLogger 的结构化日志 api 类型灵活,并接受可变的键值对的数量。

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

4.创建Logger

4.1 创建Logger几种方式

Zap中通过调用zap.NewProduction()zap.NewDevelopment()或者zap.Example()可创建一个Logger

他们创建的Logger,唯一的区别在于它将记录的信息不同。

使用场景如下:

  • zap.NewProduction() : 在生产环境中使用
  • zap.NewDevelopment() : 在开发环境中使用
  • zap.Example() : 适合用在测试代码中

4.2 使用示例

a.代码
func TestCreateLogger(t *testing.T) {
// 初始化logger
logger := zap.NewExample()
// 使用defer logger.Sync()将缓存同步到文件中。
defer logger.Sync()
// 记录日志
logger.Info("NewExample",
zap.String("name","张三"),
zap.Int("age",18),
)
productionLogger, _ := zap.NewProduction()
defer productionLogger.Sync()
productionLogger.Info("NewProduction",
zap.String("name","张三"),
zap.Int("age",18),
)
devLogger, _ := zap.NewDevelopment()
defer devLogger.Sync()
devLogger.Info("NewDevelopment",
zap.String("name","张三"),
zap.Int("age",18),
)
}
b. 输出
=== RUN   TestCreateLogger
{"level":"info","msg":"NewExample","name":"张三","age":18}
{"level":"info","ts":1624005421.7035909,"caller":"test/zap_test.go:25","msg":"NewProduction","name":"张三","age":18}
2021-06-18T16:37:01.703+0800 INFO test/zap_test.go:31 NewDevelopment {"name": "张三", "age": 18}
--- PASS: TestCreateLogger (0.00s)
PASS

zap底层 API 可以设置缓存,所以一般使用defer logger.Sync()将缓存同步到文件中

4.3 总结

  1. 使用NewProduction()记录日志,默认会记录调用函数信息、日期和时间。
  2. NewExampleNewProduction() 默认都是使用json格式记录日志,而NewDevelopment不是。
  3. 默认情况下日志都会打印到应用程序的控制台界面。
  4. 记录日志时,尽量调用zap.T(key,val)对应的类型方法,这也是zap高性能原因的一部分。

5.记录日志

5.1 使用默认记录器(Logger)

a.代码示例
// 使用默认记录日志
func TestRecordLogWithDefault(t *testing.T) {
// 初始化记录器(使用默认记录器)
logger := zap.NewExample()
defer logger.Sync()
// 记录日志
logger.Debug("这是debug日志")
logger.Debug("这是debug日志",zap.String("name","张三"))
logger.Info("这是info日志",zap.Int("age",18))
logger.Error("这是error日志",zap.Int("line",130),zap.Error(fmt.Errorf("错误示例")))
logger.Warn("这是Warn日志")
// 下面两个都会中断程序
//logger.Fatal("这是Fatal日志")
//logger.Panic("这是Panic日志")
}
b.输出
=== RUN   TestRecordLogWithDefault
{"level":"debug","msg":"这是debug日志"}
{"level":"debug","msg":"这是debug日志","name":"张三"}
{"level":"info","msg":"这是info日志","age":18}
{"level":"error","msg":"这是error日志","line":130,"error":"错误示例"}
{"level":"warn","msg":"这是Warn日志"}
--- PASS: TestRecordLogWithDefault (0.00s)

5.2 使用默认记录器(Sugar)

a.代码示例
// 使用Sugar记录器
func TestRecordLogWithSuage(t *testing.T) {
// 初始化记录器
logger := zap.NewExample()
// 把日志记录器转成Sugar
sugarLogger := logger.Sugar()
defer sugarLogger.Sync()
// 记录日志
sugarLogger.Debug("这是debug日志 ",zap.String("name","张三"))
sugarLogger.Debugf("这是Debugf日志 name:%s ","张三")
sugarLogger.Info("这是info日志",zap.Int("age",18))
sugarLogger.Infof("这是Infof日志 内容:%v",map[string]string{"爱好":"动漫"})
sugarLogger.Error("这是error日志",zap.Int("line",130),zap.Error(fmt.Errorf("错误示例")))
sugarLogger.Errorf("这是Errorf日志,错误信息:%s","错误报告!")
sugarLogger.Warn("这是Warn日志")
sugarLogger.Warnf("这是Warnf日志 %v",[]int{1,2,4,5})
// 下面两个都会中断程序
//sugarLogger.Fatal("这是Fatal日志")
//sugarLogger.Panic("这是Panic日志")
}
b.输出
=== RUN   TestRecordLogWithSuage
{"level":"debug","msg":"这是debug日志 {name 15 0 张三 <nil>}"}
{"level":"debug","msg":"这是Debugf日志 name:张三 "}
{"level":"info","msg":"这是info日志{age 11 18 <nil>}"}
{"level":"info","msg":"这是Infof日志 内容:map[爱好:动漫]"}
{"level":"error","msg":"这是error日志{line 11 130 <nil>} {error 26 0 错误示例}"}
{"level":"error","msg":"这是Errorf日志,错误信息:错误报告!"}
{"level":"warn","msg":"这是Warn日志"}
{"level":"warn","msg":"这是Warnf日志 [1 2 4 5]"}
--- PASS: TestRecordLogWithSuage (0.00s)
PASS

6.定制Logger

除了zap.NewProduction()zap.NewDevelopment()zap.Example()还可以通过zap.New(...)创建一个Logger

6.1 定制一: 输出到文件

a.代码示例
func Test2File(t *testing.T) {
// 指定写入文件
fileHandle, _ := os.Create("./test.log")
writeFile := zapcore.AddSync(fileHandle)
// 设置日志输出格式为JSON (参数复用NewDevelopmentEncoderConfig)
encoder := zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig())
// 返回zapcore.Core,并指定记录zap.DebugLevel级别及以上日志
zcore := zapcore.NewCore(encoder, zapcore.Lock(writeFile), zap.DebugLevel)
// 创建日志记录器
logger := zap.New(zcore)
defer logger.Sync()
// 记录日志
logger.Info("输出日志到文件", zap.String("name", "张三"))
}

6.2 定制二: 同时输入文件和控制台

// 同时输入到文件和控制台
func TestPrintFileAndStd(t *testing.T) {
// 指定写入文件
fileHandle, _ := os.Create("./test.log")
// 同时写入文件和控制台 (只修改这一行)
writeFile := zapcore.NewMultiWriteSyncer(fileHandle,os.Stdout)
// 设置日志输出格式为JSON (参数复用NewDevelopmentEncoderConfig)
encoder := zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig())
// 返回zapcore.Core
zcore := zapcore.NewCore(encoder, zapcore.Lock(writeFile), zap.DebugLevel)
// 创建日志记录器
logger := zap.New(zcore)
defer logger.Sync()
// 记录日志
logger.Info("输出日志到文件", zap.String("name", "张三"))
}

7.切割日志

Zap本身不支持文件切割和日志归档,好在开源强大,贡献出Lumberjack,它是一个Go包,用于将日志写入滚动文件。

7.1 安装Lumberjack

go get -u github.com/natefinch/lumberjack

7.2 集成到Zap

a. 代码示例
// 获取文件切割和归档配置信息
func getLumberjackConfig() zapcore.WriteSyncer {
lumberjackLogger := &lumberjack.Logger{
Filename: "./zap.log",//日志文件
MaxSize: 1,//单文件最大容量(单位MB)
MaxBackups: 3,//保留旧文件的最大数量
MaxAge: 1,// 旧文件最多保存几天
Compress: false, // 是否压缩/归档旧文件
}
return zapcore.AddSync(lumberjackLogger)
}

// 测试日志切割和归档
func TestCutAndArchive(t *testing.T) {
// 设置日志输出格式为JSON (参数复用NewDevelopmentEncoderConfig)
encoder := zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig())
core := zapcore.NewCore(encoder, getLumberjackConfig(), zap.DebugLevel)
sugarLogger := zap.New(core).Sugar()
defer sugarLogger.Sync()
// 记录日志
sugarLogger.Infof("日志内容:%s",strings.Repeat("日志",90000))
}
b. 效果

在代码中设置单文件最大容量为1MB,如上图所示当文件超过1MB(527+527 > 1024)时,则分割。

c. 没有记录CallerStacktrace

定制后的日志记录,发现没有记录日志打印的行号,以及错误时没有记录Stacktrace,需要按照以下改进:

// 修改 zap.New(core),改成以下内容:
zap.New(core,zap.AddCaller(),zap.AddStacktrace(zap.ErrorLevel))

日志效果:

# 修改前
{"level":"warn","ts":1627012281.7510028,"msg":"解析JWT失败","error":"token contains an invalid number of segments"}
# 修改后
{"level":"WARN","time":"2021/07/23 - 15:29:43.780","caller":"middleware/jwt.go:118","msg":"解析JWT失败","error":"token contains an invalid number of segments"}