Uber开源之高性能日志库(zap)

目录
  1. 1. 1.介绍
  2. 2. 2.安装
  3. 3. 3.日志记录器
  4. 4. 4.创建Logger
    1. 4.1. 4.1 创建Logger几种方式
    2. 4.2. 4.2 使用示例
      1. 4.2.1. a.代码
      2. 4.2.2. b. 输出
    3. 4.3. 4.3 总结
  5. 5. 5.记录日志
    1. 5.1. 5.1 使用默认记录器(Logger)
      1. 5.1.1. a.代码示例
      2. 5.1.2. b.输出
    2. 5.2. 5.2 使用默认记录器(Sugar)
      1. 5.2.1. a.代码示例
      2. 5.2.2. b.输出
  6. 6. 6.定制Logger
    1. 6.1. 6.1 定制一: 输出到文件
      1. 6.1.1. a.代码示例
    2. 6.2. 6.2 定制二: 同时输入文件和控制台
  7. 7. 7.切割日志
    1. 7.1. 7.1 安装Lumberjack
    2. 7.2. 7.2 集成到Zap
      1. 7.2.1. c. 没有记录Caller和Stacktrace

1.介绍

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

2.安装

1
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.代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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. 输出
1
2
3
4
5
6
7
8
9
10
=== 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.代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用默认记录日志
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.输出
1
2
3
4
5
6
7
=== 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.代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用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.输出
1
2
3
4
5
6
7
8
9
10
11
=== 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.代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 定制二: 同时输入文件和控制台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 同时输入到文件和控制台
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

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

7.2 集成到Zap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取文件切割和归档配置信息
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))
}

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

c. 没有记录CallerStacktrace

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

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

日志效果:

1
2
3
4
5
# 修改前
{"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"}