一.前言
在之前的文章中我们已经介绍过如何使用logrus
包来作为我们在gin框架中使用的日志中间件,而今天我们要介绍的就是我们如何在go项目中如何集成Zap
来作为日志中间件
二.Zap的安装与快速使用
和安装其他第三方包没什么区别,我们下载Zap
包只需要执行以下命令
go get -u go.uber.org/zap
在Zap的矿方说明中,给出了两种类型的日志记录器——Logger
与Sugared Logger
,在不同场景下我们可以选择使用不同的日志记录器:
Logger
:性能比较好,但是仅支持强类型输出的日志,适合在每一微秒和每一次内存分配都很重要的上下文中,使用Logger
Sugared Logger
:它支持结构化和printf风格的日志记录。适合在性能很好但不是很关键的上下文中,使用SugaredLogger
接下来我将用两个简单的demo来展示一下我们如何使用这两种日志记录器:
//Logger
package main
import (
"go.uber.org/zap"
"net/http"
)
var logger *zap.Logger
func main() {
InitLogger()
defer logger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
SimpleHttpGet("www.baidu.com") //这行代码会报错,仅做展示错误日志信息的打印
SimpleHttpGet("https://www.kugou.com")
}
func InitLogger() {
logger, _ = zap.NewProduction()
}
func SimpleHttpGet(url string) {
re, err := http.Get(url)
if err != nil {
logger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
} else {
logger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
}
re.Body.Close()
}
//SugarLogger
package main
import (
"go.uber.org/zap"
"net/http"
)
var sugarlogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
SimpleHttpGet("www.baidu.com")
SimpleHttpGet("https://www.kugou.com")
}
func InitLogger() {
logger, _ := zap.NewProduction()
sugarlogger = logger.Sugar()
}
func SimpleHttpGet(url string) {
re, err := http.Get(url)
if err != nil {
sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
} else {
sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
}
re.Body.Close()
}
三.Zap的配置
Zap的使用其实是比较简单的,但是如何去配置出一个适合我们自己项目的日志中间件其实也是比较困难,下面博主将一步步的实现的一个简单的日志中间件示例,下面开始吧!
1.让日志输入到文件中
在我们日常开发模式时,我们一般会将日志的错误信息打印在控制台上,这样可以方便我们去调试错误,但是在生产模式下,让错误信息打印在控制台上无疑是不大可能得了,我们一般会选择将日志信息录入到文件中,接下来让我们来尝试一下修改日志信息的输入路径。
为了修改日志信息的输出路径,我们这里就不会在通过NewProduction
来自动创建logger
对象了,而是我们自己通过New
这一函数来手动传递配置了,在开始之前我们来看一下New
这一函数:
func New(core zapcore.Core, options ...Option)
而这里我们所要配置的就是core
,zapcore.Core
需要三个配置:
Encoder
:它决定了我们以何种形式写入日志,比如我们可以使用Json
格式来作为我们书写日志的格式WriteSyncer
:它决定了我们要将日志写到什么地方去LogLevel
:它决定了哪些级别的日志会被写入到日志文件中去
接下来我们尝试将日志打印在test.log
中,并且用Json
和text
两种格式来打印到文件中:
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net/http"
"os"
)
var sugarlogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
SimpleHttpGet("www.baidu.com")
SimpleHttpGet("https://www.kugou.com")
}
func InitLogger() {
encoder := InitEncoder()
level := InitLevel()
writer := InitWriter()
core := zapcore.NewCore(encoder, writer, level)
sugarlogger = zap.New(core).Sugar()
}
func InitEncoder() zapcore.Encoder {
return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
func InitWriter() zapcore.WriteSyncer {
file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\test.log")
return zapcore.AddSync(file)
}
func InitLevel() zapcore.Level {
return zapcore.ErrorLevel
}
func SimpleHttpGet(url string) {
re, err := http.Get(url)
if err != nil {
sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
} else {
sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
}
re.Body.Close()
}
运行结果如下:
我们可以看到文件已经输入到json.log
中了。
当然我们也可以使用正常的text
格式
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net/http"
"os"
)
var sugarlogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
SimpleHttpGet("www.baidu.com")
SimpleHttpGet("https://www.kugou.com")
}
func InitLogger() {
encoder := InitEncoder()
level := InitLevel()
writer := InitWriter()
core := zapcore.NewCore(encoder, writer, level)
sugarlogger = zap.New(core).Sugar()
}
func InitEncoder() zapcore.Encoder {
return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}
func InitWriter() zapcore.WriteSyncer {
file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")
return zapcore.AddSync(file)
}
func InitLevel() zapcore.Level {
return zapcore.ErrorLevel
}
func SimpleHttpGet(url string) {
re, err := http.Get(url)
if err != nil {
sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
} else {
sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
}
re.Body.Close()
}
运行结果:
2.修改时间编码,将调用函数信息记录在日志中
在上面日志输出中我们可以看到两个比较大的问题:
- 时间是以非人类可读的方式展示,像
1.7233697314262748e+09
这样 - 日志没有调用方的信息我们很难确定错误的位置
所以我们现在要做的就是以下修改:
- 修改时间编码器
- 让日志文件中存在调用者信息
首先是修改时间编码器:
func InitEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别
return zapcore.NewConsoleEncoder(encoderConfig)
}
运行结果如下:
2024-08-11T18:14:02.328+0800 INFO Success fetching url{statusCode 15 0 403 Forbidden <nil>} {url 15 0 https://www.kugou.com <nil>}
最后我们添加将调用函数信息记录到日志中的功能,这里我们需要修改一下代码:
func InitLogger() {
encoder := InitEncoder()
level := InitLevel()
writer := InitWriter()
core := zapcore.NewCore(encoder, writer, level)
sugarlogger = zap.New(core,zap.AddCaller()).Sugar()
}
这样我们就能看到调用信息了:
2024-08-11T19:00:52.831+0800 INFO main/main.go:47 Success fetching url{statusCode 15 0 403 Forbidden <nil>} {url 15 0 https://www.kugou.com <nil>}
拓展:AddCallerSkip
在日志记录过程中,我们通常希望在日志消息中包含准确的调用信息(即,记录日志的代码行)。例如,如果你在 main.go 文件的第 10 行调用了 logger.Info(“Message”),你希望日志记录显示调用发生在 main.go:10。
但是,如果你将日志记录封装到另一个函数中,例如:
func logInfo(msg string) {
logger.Info(msg)
}
这样返回的值就不是准确的日志记录问题了,因为日志记录的调用栈就会增加一层,因为实际上 logger.Info
是由 logInfo 函数
调用的。如果不调整调用栈深度,日志中可能会显示 logInfo 函数的调用位置
,而不是实际的日志记录位置。
而AddCallerSkip 函数
用于调整日志记录库中记录调用信息的调用栈深度。它可以让你指定跳过多少层调用栈,从而准确获取实际的日志记录位置。
所以我们最后的InitLogger
函数是这样的:
func InitLogger() {
encoder := InitEncoder()
level := InitLevel()
writer := InitWriter()
core := zapcore.NewCore(encoder, writer, level)
sugarlogger = zap.New(core, zap.AddCaller(),zap.AddCallerSkip(1)).Sugar()
}
3.如何将日志输出到多个位置或将特定级别日志输入到单独文件
- 将日志输出到多个位置
func InitWriter() zapcore.WriteSyncer {
file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")
os := io.MultiWriter(os.Stdout, file) //既输入到控制台也输入到日志文件中
return zapcore.AddSync(os)
}
- 将特定级别日志输入到单独文件(以error为例)
func InitLogger() {
encoder := InitEncoder()
level := InitLevel()
writer := InitWriter()
c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志
errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")
c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志
core := zapcore.NewTee(c1, c2) // tee将日志输出到多个目的地
sugarlogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)).Sugar()
}
以上完整代码如下:
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io"
"net/http"
"os"
)
var sugarlogger *zap.SugaredLogger
func main() {
InitLogger()
defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
SimpleHttpGet("https://www.kugou.com")
SimpleHttpGet("www.baidu.com")
}
func InitLogger() {
encoder := InitEncoder()
level := InitLevel()
writer := InitWriter()
c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志
errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")
c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志
core := zapcore.NewTee(c1, c2) // tee将日志输出到多个目的地
sugarlogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)).Sugar()
}
func InitEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别
return zapcore.NewConsoleEncoder(encoderConfig)
}
func InitWriter() zapcore.WriteSyncer {
file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")
os := io.MultiWriter(os.Stdout, file)
return zapcore.AddSync(os)
}
func InitLevel() zapcore.Level {
return zapcore.DebugLevel
}
func SimpleHttpGet(url string) {
re, err := http.Get(url)
if err != nil {
sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
} else {
sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
}
re.Body.Close()
}
四.Zap实现日志分割
日志切割可以使用Lumberjack
这一第三方包,可以按照下面这个命令下载:
go get gopkg.in/natefinch/lumberjack.v2
最后我们来开一下怎么加入支持:
func InitWriter() zapcore.WriteSyncer {
lumberjackLogger := &lumberjack.Logger{
Filename: "G:\\bluebell\\src\\demo\\log\\app.log", //日志文件路径
MaxSize: 1, //每个日志文件保存的最大尺寸 单位:MB
MaxBackups: 5, //最多保存多少个日志文件
MaxAge: 30, //日志文件最多保存多少天
Compress: false, //是否压缩
}
return zapcore.AddSync(lumberjackLogger)
}
这样就实现了一个简单的日志分割了。
五.在gin框架中集成Zap日志库
在很早之前博主就写过gin.Default
会调用Logger(), Recovery()
这两个中间件,,所我们想在gin框架中集成Zap日志库只需要重写一下这两个中间件就可以了:
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
logger.Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
// GinRecovery recover掉项目可能出现的panic
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: err check
c.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
最后得到的就是我们的最终log文件
代码:
package main
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
)
var logger *zap.Logger
func main() {
r := gin.New()
r.Use(GinLogger(), GinRecovery(true))
}
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
logger.Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
// GinRecovery recover掉项目可能出现的panic
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: err check
c.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
func InitLogger() {
encoder := InitEncoder()
level := InitLevel()
writer := InitWriter()
c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志
errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")
c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志
core := zapcore.NewTee(c1, c2) // tee将日志输出到多个目的地
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
}
func InitEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别
return zapcore.NewConsoleEncoder(encoderConfig)
}
func InitWriter() zapcore.WriteSyncer {
lumberjackLogger := &lumberjack.Logger{
Filename: "G:\\bluebell\\src\\demo\\log\\app.log", //日志文件路径
MaxSize: 1, //每个日志文件保存的最大尺寸 单位:MB
MaxBackups: 5, //最多保存多少个日志文件
MaxAge: 30, //日志文件最多保存多少天
Compress: false, //是否压缩
}
return zapcore.AddSync(lumberjackLogger)
}
func InitLevel() zapcore.Level {
return zapcore.DebugLevel
}