在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能:
Go语言提供的默认日志包是:https://golang.org/pkg/log/。
我们可以像下面代码一样设置日志记录器
func SetupLogger(){
logFileLocation,_ := os.OpenFile("/Users/alblue/test.log",os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
log.SetOutput(logFileLocation)
}
让我们来写一些虚拟的代码来使用这个日志记录器。
在当前的示例中,我们将建立一个到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()
}
}
现在让我们执行上面的代码并查看日志记录器的运行情况。
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
优势
最大的优点就是使用非常简单。我们可以设置任何的io.Writer
作为日志记录输出并向其发送要写入的日志。
劣势
仅限基本的日志级别
Print
选项。不支持INFO
/DEBUG
等多个级别。对于错误日志,它有Fatal
和Panic
os.Exit(1)
来结束程序缺乏日志格式化的能力——例如记录调用者的函数名和行号,格式化日期和时间格式。等等。
不提供日志切割的能力。
根据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提供了两种类型的日志记录器- Sugared Logger
和Logger
。
在性能很好但不是很关键的上下文中,使用SugaredLogger
。它比其他结构化日志记录包快4-10。并且支持结构化和printf风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用Logger
。它甚至比SugaredLogger
更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
工作中一般使用Logger就行。
zap.NewProduction()
/zap.NewDevelopment()
或者zap.Example()
创建一个Logger。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]}
我们要做的第一个更改是把日志写入文件,而不是打印到应用程序控制台。
我们将使用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级别的日志写在一个文件中。
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更改为普通的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的日志
鉴于我们对配置所做的更改,有下面的两个问题:
我们要做的第一件事情就是覆盖默认的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中加入lumberjack支持,我们需要修改WriteSyncser
代码。我们将按照下面的代码修改:
lumberJackLogger := &lumberjack.Logger{
Filename: "./test.log",
MaxSize: 10,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
zapcore.AddSync(lumberJackLogger)
Lumberjack Logger采用以下属性作为输入:
两种比较特殊的配置场景:
xx.log
,同时将err级别的日志输出到xx.err.log
里