我们使用日志的目的是什么?
- 想知道系统在运行过程中发生什么,是在哪里发生的
- 想知道发生的时间
- 想知道发生事件的级别是什么
共有8个级别,按照从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF。
All:最低等级的,用于打开所有日志记录.
Trace:是追踪,就是程序推进一下.
Debug:指出细粒度信息事件对调试应用程序是非常有帮助的.
Info:消息在粗粒度级别上突出强调应用程序的运行过程.
Warn:输出警告及warn以下级别的日志.
Error:输出错误信息日志.
Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志.
OFF:最高等级的,用于关闭所有日志记录.
程序会打印高于或等于所设置级别的日志,设置的日志等级越高,打印出来的日志就越少
在学习Zap之前我们先对go自带的log包做一下介绍。
log包
搭建简单gin项目
首先我们先搭建一个简单的gin项目
$ go get -u github.com/gin-gonic/gin
然后
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run(":9000") // 监听并在 0.0.0.0:9000 上启动服务 }
启动项目并访问:http://localhost:9000/ping,如果出现如下信息就是搭建成功了。
配置日志
我们如果想要log的输出在某一个文件中,那么我们就需要给程序指定一个文件的路径,如下面的程序。
package utils import ( "log" "os" ) // SetupLogger 启动logger func SetupLogger() { outFile, _ := os.OpenFile("./log.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744) // 设置log输出位置 log.SetOutput(outFile) }
我们将访问者的ip输出到log文件中,那么我们还需要获取访问者的ip。
此段代码出处:go 获取http请求中的ip地址,博主地址:weishunuan
package utils import "net/http" // RemoteIp 获取请求中的IP func RemoteIp(req *http.Request) string { var remoteAddr string // RemoteAddr remoteAddr = req.RemoteAddr if remoteAddr != "" { return remoteAddr } // ipv4 remoteAddr = req.Header.Get("ipv4") if remoteAddr != "" { return remoteAddr } // remoteAddr = req.Header.Get("XForwardedFor") if remoteAddr != "" { return remoteAddr } // X-Forwarded-For remoteAddr = req.Header.Get("X-Forwarded-For") if remoteAddr != "" { return remoteAddr } // X-Real-Ip remoteAddr = req.Header.Get("X-Real-Ip") if remoteAddr != "" { return remoteAddr } else { remoteAddr = "127.0.0.1" } return remoteAddr }
测试log
在主方法中启动刚才的日志配置。
package main import ( "github.com/gin-gonic/gin" "log" "log/logtest/utils" "net/http" ) func main() { // 启动日志 utils.SetupLogger() r := gin.Default() r.GET("/testLog", func(c *gin.Context) { // 将访问者ip打印到日志中 ip := utils.RemoteIp(c.Request) log.Println(ip) c.JSON(http.StatusOK,"访问者ip为"+ip) }) r.Run(":9000") }
访问http://localhost:9000/testLog,如果输出
查看项目下生成的log.log文件
这便把项目运行结果输出到日志文件中了。
log的优缺点
- 优点
- 可以使用任何的 io.Writer 作为指定的输出目标,也就说可以指定的文件种类多种多样,输出平台也多种多样。
- 简单易用
- 缺点
- 输出只有一个print选项,不支持常使用的INFO/DEBUG
- 使用Panic方法会报出panic
// 等价于Print,但是执行后会报出panic func Panic(v ...interface{}) { s := fmt.Sprint(v...) std.Output(2, s) panic(s) }
- 使用 Fatal 方法会使程序退出
// 等价于Print,但是执行后会退出程序 func Fatal(v ...interface{}) { std.Output(2, fmt.Sprint(v...)) os.Exit(1) }
- 没有ERROR日志级别,不能再不抛出panic和退出程序下记录错误
- 信息量简单,缺乏我们想要的信息
- 没有日志切割功能,也就是说扩展麻烦
Zap包
了解了log的使用还有优缺点后,我们发现,log包是无法满足我们平常日志工作的需要的,所以我们可以重新引用一个包,它便是Uber-go Zap
。
注意,zap只支持Go的两个最新的小版本。
优点
使用原因:
- 快得惊人
- 结构清晰
- 等级分明
快速
对于加载到热路径的应用程序,基于反射的序列化和字符串格式化对性能的需求—它们是cpu密集型的,需要进行许多小的分配。换句话说,使用encoding/json
和fmt.Fprintf
将大量的接口信息输出到日志会使你的应用程序变慢。
Zap采取了不同的方法。它包括一个无反射、零分配的JSON编码器,基础Logger努力避免序列化开销和分配。通过在此基础上构建高级的SugaredLogger, zap允许用户选择何时需要计算每个分配,以及何时需要更熟悉的松散类型的API。
zap不仅比可比较的结构化日志包性能更好,而且比标准库(上面我们使用的log包)更快。
它的速度体现于
- 记录一条信息和10个字段:
包 | 用时 | 相对于zap用时 | 对象分配 |
⚡ zap | 2900 ns/op | +0% | 5 allocs/op |
⚡ zap (sugared) | 3475 ns/op | +20% | 10 allocs/op |
zerolog | 10639 ns/op | +267% | 32 allocs/op |
go-kit | 14434 ns/op | +398% | 59 allocs/op |
logrus | 17104 ns/op | +490% | 81 allocs/op |
apex/log | 32424 ns/op | +1018% | 66 allocs/op |
log15 | 33579 ns/op | +1058% | 76 allocs/op |
- 使用已经有10个上下文字段的日志记录器记录:
包 | 用时 | 相对于zap用时 | 对象分配 |
⚡ zap | 373 ns/op | +0% | 0 allocs/op |
⚡ zap (sugared) | 452 ns/op | +21% | 1 allocs/op |
zerolog | 288 ns/op | -23% | 0 allocs/op |
go-kit | 11785 ns/op | +3060% | 58 allocs/op |
logrus | 19629 ns/op | +5162% | 70 allocs/op |
log15 | 21866 ns/op | +5762% | 72 allocs/op |
apex/log | 30890 ns/op | +8182% | 55 allocs/op |
- 记录一个静态字符串,不需要任何上下文或printf风格的模板:
包 | 用时 | 相对于zap | 对象分配 |
⚡ zap | 381 ns/op | +0% | 0 allocs/op |
⚡ zap (sugared) | 410 ns/op | +8% | 1 allocs/op |
zerolog | 369 ns/op | -3% | 0 allocs/op |
standard library | 385 ns/op | +1% | 2 allocs/op |
go-kit | 606 ns/op | +59% | 11 allocs/op |
logrus | 1730 ns/op | +354% | 25 allocs/op |
apex/log | 1998 ns/op | +424% | 7 allocs/op |
log15 | 4546 ns/op | +1093% | 22 allocs/op |
根据上面的表格可以发现,zap速度是非常迅速的。
稳定性
zap中所有api都完成了,在第1版本中不会有任何破坏性的更改。对于依赖管理系统的用户应该把zap固定到^1。
使用
安装
$ go get -u go.uber.org/zap
编写记录器(Logger)
// SetupZapLogger 启动Zap记录器 func SetupZapLogger() *zap.Logger { // 输出InfoLevel 及以上级别 logger, _ := zap.NewProduction() // 输出DebugLevel 及以上级别 //logger, _ := zap.NewDevelopment() // 输出DebugLevel 及以上级别,但是省略了时间戳和调用函数,来保持输出的简洁明了 //logger := zap.NewExample() return logger }
zap有三种快速生成记录器的方法
zap.NewProduction()
记录器的输出格式,以json格式记录,而起时间还是时间戳,不方便阅读
zap.NewDevelopment()
记录器的输出格式
zap.NewExample()
记录器的输出格式,简单日志,同样是json格式
main.go
package main import ( "github.com/gin-gonic/gin" "log/logtest/utils" "net/http" ) func main() { // 启动记录器 logger := utils.SetupZapLogger() // 刷新所有缓存的条目,应该在程序结束之前使用 defer logger.Sync() r := gin.Default() r.GET("/testZap", func(c *gin.Context) { // 将访问者ip打印到日志中 ip := utils.RemoteIp(c.Request) // InfoLevel logger.Info(ip) // PanicLevel // 不可恢复的panic,即使没有使用PanicLevel,当产生panic时也会产生记录 // 但是没有json格式的信息输出 //logger.Panic(ip) //panic("恐慌了") // ErrorLevel //logger.Error(ip) // DebugLevel //logger.Debug(ip) // FatalLevel // 会退出程序 //logger.Fatal(ip) // DPanicLevel // 在开发阶段使用,指可恢复但是不应该发生的panic //logger.DPanic(ip) // WarnLevel //logger.Warn(ip) c.JSON(http.StatusOK,"访问者ip为"+ip) }) r.Run(":9000") }
如果没有指定输出位置的话,默认输出到控制台。
加糖
我们也可以给记录器加糖,加糖之后的记录器sugarLogger可以使用printf格式化输出等方法,如果用不到printf方法的话就不建议用,毕竟会损失一定的速度。
package main import ( "github.com/gin-gonic/gin" "log/logtest/utils" "net/http" ) func main() { // 启动记录器 logger := utils.SetupZapLogger() // 刷新所有缓存的条目,应该在程序结束之前使用 defer logger.Sync() r := gin.Default() r.GET("/testLogger", func(c *gin.Context) { // 将访问者ip打印到日志中 ip := utils.RemoteIp(c.Request) logger.Info("访问者ip", // 结构化上下文作为强类型字段值。 zap.String("ip", ip), ) c.JSON(http.StatusOK,"访问者ip为"+ip) }) // 加糖后更符合工程学,而且加糖的消耗是非常小的,但是会拖慢执行速度 sugar := logger.Sugar() r.GET("/testSugarLogger", func(c *gin.Context) { // 将访问者ip打印到日志中 ip := utils.RemoteIp(c.Request) sugar.Infow("获取访问者ip", // 以结构化的上下文作为松散的键值对 "ip", ip, ) c.JSON(http.StatusOK,"访问者ip为"+ip) }) r.Run(":9000") }
加糖后松散结构的键值对显然比加糖前的强关联型键值对易用。但是会损失一定的速度,但是这个损失是很小的,就算是加糖后的记录器也要比其他的日志记录器快上4~10倍。
指定输出地址
日志的输出位置不应该是终端,所以我们要给记录器指定一个输出地址。
指定输出地址就不能使用简单生成记录器的方法了,而是要使用zap.New()
传入参数手动配置我们需要的属性。
func New(core zapcore.Core, options ...Option) *Logger
其中实例化core需要有Encoder、WriteSyncer、LevelEnabler这三个参数
// NewCore creates a Core that writes logs to a WriteSyncer. func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core { return &ioCore{ LevelEnabler: enab, enc: enc, out: ws, } }
- Encoder:编码器(如何写入日志)。我们将使用开箱即用的
NewJSONEncoder()
,并使用预先设置的ProductionEncoderConfig()
。
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
- WriterSyncer :指定日志将写到哪里去。我们使用
zapcore.AddSync()
函数并且将打开的文件句柄传进去。
file, _ := os.Create("./test.log") writeSyncer := zapcore.AddSync(file)
- Log Level:哪种级别及以上的日志将被写入。
我们将启动方法改为如下:
// SetupZapLogger 启动Zap记录器 func SetupZapLogger() *zap.Logger { encoder := getEncoder() writeSyncer := getWriteSyncer() return zap.New(zapcore.NewCore(encoder,writeSyncer,zapcore.DebugLevel)) } func getEncoder() zapcore.Encoder { return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) } func getWriteSyncer() zapcore.WriteSyncer { file, _ := os.Create("./zapLog.log") return zapcore.AddSync(file) }
但是main方法不动,启动并访问http://localhost:9000/testSugarLogger后就可以看见生成的zapLog.log中有内容了。如下:
{“level”:“info”,“ts”:1652168200.6373003,“msg”:“获取访问者ip”,“ip”:“[::1]:57239”}
{“level”:“info”,“ts”:1652168298.905223,“msg”:“获取访问者ip”,“ip”:“[::1]:57269”}
但是这样的输出很难看,对,就是很难看,因为对于我来说没办法看到这个时间就知道是啥时候发生的,我更习惯于普通的时间格式,虽然json格式便于使用脚本分析,但是对于我这个人来说json格式的输出看着并不舒服(如果你喜欢这样的哪当我没说),我想让其改成普通控制台的输出格式。
更改编码达到预期格式
- 将json格式的输出–>普通控制台的输出格式
zapcore.NewJSONEncoder() ---> zapcore.NewConsoleEncoder()
- 覆盖默认的编码格式修改时间表示
encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
那么我们的启动代码应该改为
// SetupZapLogger 启动Zap记录器 func SetupZapLogger() *zap.Logger { encoder := getEncoder() writeSyncer := getWriteSyncer() return zap.New(zapcore.NewCore(encoder,writeSyncer,zapcore.DebugLevel)) } func getEncoder() zapcore.Encoder { // 覆盖NewProductionEncoderConfig()默认的编码设置,改为自定义的 encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 以json格式的方式输出日志 //encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) // 以普通方式输出日志 encoder := zapcore.NewConsoleEncoder(encoderConfig) return encoder } func getWriteSyncer() zapcore.WriteSyncer { file, _ := os.Create("./zapLog.log") return zapcore.AddSync(file) }
新的输出格式
2022-05-10T15:57:38.199+0800 info 获取访问者ip {“ip”: “[::1]:51507”}
日志切割归档
不管是标准库的log包还是zap包,都不包含日志切割归档的功能。
我们可以引入Lumberjack
包,实现这一功能。
安装
$ go get -u github.com/natefinch/lumberjack
使用
我们要在zap中使用Lumberjack非常简单,只需要更改一下指定输出目标的代码,如下
func getWriteSyncer() zapcore.WriteSyncer { //file, _ := os.Create("./zapLog.log") logger := lumberjack.Logger{ Filename: "./zapLog.log", // 日志文件每1MB会切割并且在当前目录下最多保存5个备份 MaxSize: 1, MaxBackups: 5, MaxAge: 30, Compress: false, //参数含义 //Filename: 日志文件的位置 //MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位) //MaxBackups:保留旧文件的最大个数 //MaxAges:保留旧文件的最大天数 //Compress:是否压缩/归档旧文件 } return zapcore.AddSync(&logger) }
测试
在main中循环记录日志,检查是否可以切割和归档
mian.go
func main() { logger := utils.SetupZapLogger() defer logger.Sync() for { logger.Info("测试") } }
切割结果
都看到这了,要不要给个赞啊客官。
参考文章: