在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能:
INFO,DEBUG,ERROR
默认的 Go Logger
Uber-go
https://golang.org/pkg/log/
实现 Go Logger
实现一个Go语言中的日志记录器非常简单——创建一个新的日志文件,然后设置它为日志的输出位置。
package main
import (
"fmt"
"log"
"os"
"time"
)
func main() {
//创建输出日志文件
logFile, err := os.Create("./" + time.Now().Format("20060102") + ".txt")
if err != nil {
fmt.Println(err)
}
//创建一个Logger
//参数1:日志写入目的地
//参数2:每条日志的前缀
//参数3:日志属性
loger := log.New(logFile, "test_", log.Ldate|log.Ltime|log.Lshortfile)
//Flags返回Logger的输出选项
fmt.Println(loger.Flags())
//SetFlags设置输出选项
loger.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
//返回输出前缀
fmt.Println("fix1_" + loger.Prefix())
//设置输出前缀
loger.SetPrefix("test_")
//输出一条日志
loger.Output(2, "打印一条日志信息")
//格式化输出日志
// loger.Printf("第%d行 内容:%s", 11, "我是错误")
//等价于print();os.Exit(1);
// loger.Fatal("我是错误")
//等价于print();panic();
// loger.Panic("我是错误33333333")
//log的导出函数
//导出函数基于std,std是标准错误输出
//var std = New(os.Stderr, "", LstdFlags)
//获取输出项
fmt.Println(log.Flags())
//获取前缀
fmt.Printf("fix2_" + log.Prefix())
}
Go Logger 的优势和劣势
优势
它最大的优点是使用非常简单。
io.Writer
劣势
INFO/DEBUG
Zap Logger
1. Uber-go Zap
Zap 是非常快的、结构化的,分日志级别的Go日志库。
Zap:https://github.com/uber-go/zap
2. 为什么选择 Uber-go zap
printf
根据 Uber-go Zap 的文档,它的性能比类似的结构化日志包更好——也比标准库更快。
以下是 Zap 发布的基准测试信息记录一条消息和10个字段:
printf
3. 安装
运行下面的命令安装 zap。
PS E:\go_test> go get -u go.uber.org/zap
go: downloading go.uber.org/zap v1.24.0
go: downloading go.uber.org/atomic v1.7.0
go: downloading go.uber.org/multierr v1.6.0
go: downloading go.uber.org/atomic v1.10.0
go: downloading go.uber.org/multierr v1.9.0
go: added go.uber.org/atomic v1.10.0
go: added go.uber.org/multierr v1.9.0
go: added go.uber.org/zap v1.24.0
PS E:\go_test>
4. 配置 Zap Logger
Zap 提供了两种类型的日志记录器
- Sugared Logger
- Logger
在性能很好但不是很关键的上下文中,使用 SugaredLogger。它比其他结构化日志记录包快 4-10 倍,并且支持结构化和 printf 风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger。它甚至比 SugaredLogger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
5. Logger
zap.NewProduction()/zap.NewDevelopment()zap.Example()
上面的每一个函数都将创建一个 logger。
唯一的区别在于它将记录的信息不同。例如 production logger 默认记录调用函数信息、日期和时间等。
Info/Error
默认情况下日志都会打印到应用程序的 console 界面。
package main
import (
"net/http"
"go.uber.org/zap"
)
var logger *zap.Logger
func main() {
InitLogger()
defer logger.Sync()
simpleHttpGet("www.baidu.com")
simpleHttpGet("https://www.runoob.com")
}
func InitLogger() {
logger, _ = zap.NewProduction()
}
func simpleHttpGet(url string) {
resp, err := http.Get(url)
if err != nil {
logger.Error(
"Error fetching url..",
zap.String("url", url),
zap.Error(err))
} else {
logger.Info("Success..",
zap.String("statusCode", resp.Status),
zap.String("url", url))
resp.Body.Close()
}
}
Info/ Error
日志记录器方法的语法是这样的:
func (log *Logger) MethodXXX(msg string, fields ...Field)
Info / Error/ Debug / Panic
每个 zapcore.Field 其实就是一组键值对参数。
我们执行上面的代码会得到如下输出结果:
{
"level": "error",
"ts": 1673101310.2563064,
"caller": "go_test/main.go:25",
"msg": "Error fetching url..",
"url": "www.baidu.com",
"error": "Get \"www.baidu.com\": unsupported protocol scheme \"\"",
"stacktrace": "main.simpleHttpGet\n\tE:/go_test/main.go:25\nmain.main\n\tE:/go_test/main.go:14\nruntime.main\n\tC:/Program Files/Go/src/runtime/proc.go:250"
}
{
"level": "info",
"ts": 1673101310.3977945,
"caller": "go_test/main.go:30",
"msg": "Success..",
"statusCode": "200 OK",
"url": "https://www.runoob.com"
}
6. Sugared Logger
现在让我们使用 Sugared Logger 来实现相同的功能。
- 大部分的实现基本都相同。
- 惟一的区别是,我们通过调用主 logger 的. Sugar()方法来获取一个 SugaredLogger。
- 然后使用 SugaredLogger 以 printf 格式记录语句。
下面是修改过后使用 SugaredLogger 代替Logger的代码:
package main
import (
"net/http"
"go.uber.org/zap"
)
var sugarLogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarLogger.Sync()
simpleHttpGet("https://www.baidu.com")
simpleHttpGet("https://www.csdn.net")
}
func InitLogger() {
logger, _ := zap.NewProduction()
sugarLogger = logger.Sugar()
}
func simpleHttpGet(url string) {
sugarLogger.Debugf("Trying to hit GET request for %s", url)
resp, err := http.Get(url)
if err != nil {
sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
} else {
sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
resp.Body.Close()
}
}
{
"level": "info",
"ts": 1673101999.8570902,
"caller": "go_test/main.go:29",
"msg": "Success! statusCode = 200 OK for URL https://www.baidu.com"
}
{
"level": "info",
"ts": 1673101999.9190702,
"caller": "go_test/main.go:29",
"msg": "Success! statusCode = 200 OK for URL https://www.csdn.net"
}
你应该注意到的了,到目前为止这两个logger都打印输出JSON结构格式。
定制 logger
将日志写入文件而不是终端。
我们要做的第一个更改是把日志写入文件,而不是打印到应用程序控制台。
func New(core zapcore.Core, options ...Option) *Logger
zapcore.Core 需要三个配置——Encoder,WriteSyncer,LogLevel。
1、Encoder:编码器(如何写入日志)。
NewJSONEncoder()ProductionEncoderConfig()
2、WriterSyncer :指定日志将写到哪里去。
zapcore.AddSync()
file, _ := os.Create("./test.log")
writeSyncer := zapcore.AddSync(file)
3、Log Level:哪种级别的日志将被写入。
InitLogger()
main()/SimpleHttpGet()
package main
import (
"net/http"
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var sugarLogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarLogger.Sync()
simpleHttpGet("https://www.baidu.com")
simpleHttpGet("https://c.runoob.com")
}
func InitLogger() {
writeSyncer := getLogWriter()
encoder := getEncoder()
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
logger := zap.New(core)
sugarLogger = logger.Sugar()
}
func getEncoder() zapcore.Encoder {
return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
func getLogWriter() zapcore.WriteSyncer {
//如果想要追加写入可以查看我的博客文件操作那一章
file, _ := os.Create("./test.log")
return zapcore.AddSync(file)
}
func simpleHttpGet(url string) {
sugarLogger.Debugf("Trying to hit GET request for %s", url)
resp, err := http.Get(url)
if err != nil {
sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
} else {
sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
resp.Body.Close()
}
}
当使用这些修改过的 logger 配置调用上述部分的 main() 函数时,以下输出将打印在文件 test.log 中。
{
"level": "debug",
"ts": 1673102827.8652663,
"msg": "Trying to hit GET request for https://www.baidu.com"
}
{
"level": "info",
"ts": 1673102827.9873803,
"msg": "Success! statusCode = 200 OK for URL https://www.baidu.com"
}
{
"level": "debug",
"ts": 1673102827.9873803,
"msg": "Trying to hit GET request for https://c.runoob.com"
}
{
"level": "info",
"ts": 1673102828.0684626,
"msg": "Success! statusCode = 200 OK for URL https://c.runoob.com"
}
将 JSON Encoder 更改为普通的 Log Encoder
现在,我们希望将编码器从 JSON Encoder 更改为普通 Encoder。
为此,我们需要将 NewJSONEncoder() 更改为 NewConsoleEncoder()。
return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
当使用这些修改过的 logger 配置调用上述部分的 main() 函数时,以下输出将打印在文件——test.log中。
func getEncoder() zapcore.Encoder {
// return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}
更改时间编码并添加调用者详细信息
鉴于我们对配置所做的更改,有下面两个问题:
- 时间是以非人类可读的方式展示,例如 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(..)
logger := zap.New(core, zap.AddCaller())
完整源码
package main
import (
"net/http"
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var sugarLogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarLogger.Sync()
simpleHttpGet("https://www.baidu.com")
simpleHttpGet("https://c.runoob.com")
}
func InitLogger() {
writeSyncer := getLogWriter()
encoder := getEncoder()
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
logger := zap.New(core)
sugarLogger = logger.Sugar()
}
// func getEncoder() zapcore.Encoder {
// return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
// return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
// }
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
func getLogWriter() zapcore.WriteSyncer {
//如果想要追加写入可以查看我的博客文件操作那一章
file, _ := os.Create("./test.log")
return zapcore.AddSync(file)
}
func simpleHttpGet(url string) {
sugarLogger.Debugf("Trying to hit GET request for %s", url)
resp, err := http.Get(url)
if err != nil {
sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
} else {
sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
resp.Body.Close()
}
}
日志切割归档
1. 使用 Lumberjack 进行日志切割归档
这个日志程序中唯一缺少的就是日志切割归档功能。
Zap本身不支持切割归档日志文件
为了添加日志切割归档功能,我们将使用第三方库Lumberjack来实现。
Lumberjack
https://github.com/natefinch/lumberjack
2. 安装
执行下面的命令安装 Lumberjack。
PS E:\go_test> go get -u github.com/natefinch/lumberjack
go: downloading github.com/natefinch/lumberjack v2.0.0+incompatible
go: added github.com/natefinch/lumberjack v2.0.0+incompatible
PS E:\go_test>
3. zap logger 中加入 Lumberjack
要在 zap 中加入 Lumberjack 支持,我们需要修改 WriteSyncer 代码。我们将按照下面的代码修改 getLogWriter() 函数:
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "./test.log",
MaxSize: 10,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}
Lumberjack Logger 采用以下属性作为输入:
- Filename: 日志文件的位置
- MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
- MaxBackups:保留旧文件的最大个数
- MaxAges:保留旧文件的最大天数
- Compress:是否压缩/归档旧文件
Zap/Lumberjack logger
package main
import (
"net/http"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var sugarLogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarLogger.Sync()
simpleHttpGet("https://www.baidu.com")
simpleHttpGet("https://c.runoob.com")
}
func InitLogger() {
writeSyncer := getLogWriter()
encoder := getEncoder()
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
logger := zap.New(core)
sugarLogger = logger.Sugar()
}
// func getEncoder() zapcore.Encoder {
// return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
// return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
// }
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
// func getLogWriter() zapcore.WriteSyncer {
//如果想要追加写入可以查看我的博客文件操作那一章
// file, _ := os.Create("./test.log")
// return zapcore.AddSync(file)
// }
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "./test.log",
MaxSize: 1,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}
func simpleHttpGet(url string) {
sugarLogger.Debugf("Trying to hit GET request for %s", url)
resp, err := http.Get(url)
if err != nil {
sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
} else {
sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
resp.Body.Close()
}
}