Go --- Zap日志包的使用

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Go --- Zap日志包的使用

我们使用日志的目的是什么?

  1. 想知道系统在运行过程中发生什么,是在哪里发生的
  2. 想知道发生的时间
  3. 想知道发生事件的级别是什么

日志级别

共有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/jsonfmt.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有三种快速生成记录器的方法

  1. zap.NewProduction()记录器的输出格式,以json格式记录,而起时间还是时间戳,不方便阅读

  1. zap.NewDevelopment()记录器的输出格式

  1. 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,
   }
}
  1. Encoder:编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig()
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
  1. WriterSyncer :指定日志将写到哪里去。我们使用zapcore.AddSync()函数并且将打开的文件句柄传进去。
file, _ := os.Create("./test.log") 
writeSyncer := zapcore.AddSync(file)
  1. 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格式的输出看着并不舒服(如果你喜欢这样的哪当我没说),我想让其改成普通控制台的输出格式。

更改编码达到预期格式

  1. 将json格式的输出–>普通控制台的输出格式
zapcore.NewJSONEncoder() ---> zapcore.NewConsoleEncoder()
  1. 覆盖默认的编码格式修改时间表示
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("测试")
   }
}

切割结果

都看到这了,要不要给个赞啊客官。

参考文章:

Zap - github地址

Q1mi大佬的文章


相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
1月前
|
存储 监控 算法
防止员工泄密软件中文件访问日志管理的 Go 语言 B + 树算法
B+树凭借高效范围查询与稳定插入删除性能,为防止员工泄密软件提供高响应、可追溯的日志管理方案,显著提升海量文件操作日志的存储与检索效率。
84 2
|
5月前
|
Go 开发者
Go语言包的组织与导入 -《Go语言实战指南》
本章详细介绍了Go语言中的包(Package)概念及其使用方法。包是实现代码模块化、复用性和可维护性的核心单位,内容涵盖包的基本定义、命名规则、组织结构以及导入方式。通过示例说明了如何创建和调用包,并深入讲解了`go.mod`文件对包路径的管理。此外,还提供了多种导入技巧,如别名导入、匿名导入等,帮助开发者优化代码结构与可读性。最后以表格形式总结了关键点,便于快速回顾和应用。
263 61
|
1月前
|
Java 编译器 Go
【Golang】(1)Go的运行流程步骤与包的概念
初次上手Go语言!先来了解它的运行流程吧! 在Go中对包的概念又有怎样不同的见解呢?
129 4
|
4月前
|
JSON 中间件 Go
Go语言实战指南 —— Go中的反射机制:reflect 包使用
Go语言中的反射机制通过`reflect`包实现,允许程序在运行时动态检查变量类型、获取或设置值、调用方法等。它适用于初中级开发者深入理解Go的动态能力,帮助构建通用工具、中间件和ORM系统等。
300 63
|
3月前
|
缓存 监控 安全
告别缓存击穿!Go 语言中的防并发神器:singleflight 包深度解析
在高并发场景中,多个请求同时访问同一资源易导致缓存击穿、数据库压力过大。Go 语言提供的 `singleflight` 包可将相同 key 的请求合并,仅执行一次实际操作,其余请求共享结果,有效降低系统负载。本文详解其原理、实现及典型应用场景,并附示例代码,助你掌握高并发优化技巧。
294 0
|
6月前
|
Go 持续交付 开发者
Go语言包与模块(module)的基本使用-《Go语言实战指南》
本章深入讲解Go语言中的包(Package)和模块(Module)概念。包是代码组织的最小单位,每个`.go`文件属于一个包,通过`import`实现复用;主程序包需命名为`main`。模块是Go 1.11引入的依赖管理机制,支持自动版本管理和私有/远程仓库,无需依赖GOPATH。通过实际示例,如自定义包`mathutil`和第三方模块`gin`的引入,展示其使用方法。常用命令包括`go mod init`、`go mod tidy`等,帮助开发者高效管理项目依赖。最后总结,包负责功能划分,模块实现现代化依赖管理,提升团队协作效率。
278 15
|
8月前
|
存储 JSON Go
PHP 日志系统的最佳搭档:一个 Go 写的远程日志收集服务
为了不再 SSH 上去翻日志,我写了个 Go 小脚本,用来接收远程日志。PHP 负责记录日志,Go 负责存储和展示,按天存储、支持 API 访问、可远程管理,终于能第一时间知道项目炸了。
171 10
|
6月前
|
监控 容灾 算法
阿里云 SLS 多云日志接入最佳实践:链路、成本与高可用性优化
本文探讨了如何高效、经济且可靠地将海外应用与基础设施日志统一采集至阿里云日志服务(SLS),解决全球化业务扩展中的关键挑战。重点介绍了高性能日志采集Agent(iLogtail/LoongCollector)在海外场景的应用,推荐使用LoongCollector以获得更优的稳定性和网络容错能力。同时分析了多种网络接入方案,包括公网直连、全球加速优化、阿里云内网及专线/CEN/VPN接入等,并提供了成本优化策略和多目标发送配置指导,帮助企业构建稳定、低成本、高可用的全球日志系统。
792 54
|
11月前
|
监控 安全 Apache
什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球广泛使用的Web服务器软件,支持超过30%的活跃网站。它通过接收和处理HTTP请求,与后端服务器通信,返回响应并记录日志,确保网页请求的快速准确处理。Apache日志分为访问日志和错误日志,对提升用户体验、保障安全及优化性能至关重要。EventLog Analyzer等工具可有效管理和分析这些日志,增强Web服务的安全性和可靠性。
335 9
下一篇
oss云网关配置