在许多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
等多个级别。
- 只有一个
-
对于错误日志,它有
Fatal
和Panic
- Fatal日志通过调用
os.Exit(1)
来结束程序 - Panic日志在写入日志消息之后抛出一个panic
- 但是它缺少一个ERROR日志级别,这个级别可以在不抛出panic或退出程序的情况下记录错误
- Fatal日志通过调用
-
缺乏日志格式化的能力——例如记录调用者的函数名和行号,格式化日期和时间格式。等等。
-
不提供日志切割的能力。
Uber-go Zap
为什么选择Uber-go zap
- 它同时提供了结构化日志记录和printf风格的日志记录。
- 它非常的快。
根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好——也比标准库更快。 以下是Zap发布的基准测试信息
记录一条消息和10个字段:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
⚡️ zap | 862 ns/op | +0% | 5 allocs/op |
⚡️ zap (sugared) | 1250 ns/op | +45% | 11 allocs/op |
zerolog | 4021 ns/op | +366% | 76 allocs/op |
go-kit | 4542 ns/op | +427% | 105 allocs/op |
apex/log | 26785 ns/op | +3007% | 115 allocs/op |
logrus | 29501 ns/op | +3322% | 125 allocs/op |
log15 | 29906 ns/op | +3369% | 122 allocs/op |
记录一个静态字符串,没有任何上下文或printf风格的模板:
Package | Time | Time % to zap | Objects Allocated |
---|---|---|---|
⚡️ zap | 118 ns/op | +0% | 0 allocs/op |
⚡️ zap (sugared) | 191 ns/op | +62% | 2 allocs/op |
zerolog | 93 ns/op | -21% | 0 allocs/op |
go-kit | 280 ns/op | +137% | 11 allocs/op |
standard library | 499 ns/op | +323% | 2 allocs/op |
apex/log | 1990 ns/op | +1586% | 10 allocs/op |
logrus | 3129 ns/op | +2552% | 24 allocs/op |
log15 | 3887 ns/op | +3194% | 23 allocs/op |
安装
运行下面的命令安装zap
go get -u go.uber.org/zap
配置Zap Logger
Zap提供了两种类型的日志记录器- Sugared Logger
和Logger
。
在性能很好但不是很关键的上下文中,使用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对象
- 自定义编码类型(普通文本、JSON)
- 字段的key
- 时间格式
- 日志级别的大小写
- 输出位置(文件、终端、多个文件)
- 日志级别
- 自定义基于日志级别的策略,比如忽略掉warn级别的日志(有兴趣的同学可以看一下)
两种比较特殊的配置场景:
- 同时输出日志到文件和终端
- 将全量日志输出到
xx.log
,同时将err级别的日志输出到xx.err.log
里
logger 用法
- 全局的logger对象
- zap.L()