阅读 81
zap 接收 gin 框架默认的日志

zap 接收 gin 框架默认的日志

前面两篇已经为大家介绍了golang中的日志如何使用,并在诸多日志框架库中选择了zap作为我们的日志框架,本篇将会讲解:

  1. 如何结合当下主流的Web框架gin进行请求日志的打印
  2. 对zap进行二次封装,注入trace信息,一遍我们可以在业务中查询一次请求的所有完整日志

这里是前两篇的链接:

  1. www.yuque.com/jinsesihuan…
  2. www.yuque.com/jinsesihuan…

1、gin 默认的中间件

首先我们来看一个最简单的 gin 项目:

func main() {
	r := gin.Default()
	r.GET("/hello", func(c *gin.Context) {
		c.String("hello jianfan.com!")
	})
	r.Run(
}
复制代码

接下来我们看一下gin.Default()的源码:

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
复制代码

也就是我们在使用gin.Default()的同时是用到了 gin 框架内的两个默认中间件Logger()Recovery()

其中Logger()是把 gin 框架本身的日志输出到标准输出(我们本地开发调试时在终端输出的那些日志就是它的功劳),而Recovery()是在程序出现 panic 的时候恢复现场并写入 500 响应的。

2、基于 zap 的中间件

gin框架支持用户自定义的middleware,我们可以模仿Logger()Recovery()的实现,使用我们的日志库来接收 gin 框架默认输出的日志。 这里以 zap 为例,我们实现两个中间件如下:

// GinLogger 接收gin框架默认的日志
func GinLogger(logger *zap.Logger) 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(logger *zap.Logger, 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: errcheck
					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()
	}
}
复制代码

如果不想自己实现,可以使用 github 上有别人封装好的 https://github.com/gin-contrib/zap

这样我们就可以在 gin 框架中使用我们上面定义好的两个中间件来代替 gin 框架默认的Logger()Recovery()了。

r := gin.New()
r.Use(GinLogger(), GinRecovery())
复制代码

3、增加Trace信息

3.1 定义trace信息

// 定义trace结构体
type Trace struct {
	TraceId   string  `json:"trace_id"`
	SpanId    string  `json:"span_id"`
	Caller    string  `json:"caller"`
	SrcMethod *string `json:"srcMethod,omitempty"`
	UserId    int   `json:"user_id"`
}

// 根据gin的context获取context,使log trace更加通用
func GetTraceCtx(c *gin.Context) context.Context {
	return c.MustGet(consts.TraceCtx).(context.Context)
}
复制代码

3.2 包装基于zap的日志工具

  • 包装方法将context传入
  • 解析trace信息,打印到logger中
package log

import (
	"best-practics/common"
	"best-practics/common/consts"
	"context"
	"go.uber.org/zap"
)

type LogWrapper struct {
	logger *zap.Logger
}

var Log LogWrapper

func Debug(tag string, fields ...zap.Field) {
	Log.logger.Debug(tag, fields...)
}

func DebugF(ctx context.Context, tag string, fields ...zap.Field) {
	trace := ctx.Value(consts.TraceKey).(*common.Trace)
	Log.logger.Debug(tag,
		append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
	)
}

func Info(tag string, fields ...zap.Field) {
	Log.logger.Info(tag, fields...)
}

func InfoF(ctx context.Context, tag string, fields ...zap.Field) {
	trace := ctx.Value(consts.TraceKey).(*common.Trace)
	Log.logger.Info(tag,
		append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
	)
}

func Warn(tag string, fields ...zap.Field) {
	Log.logger.Warn(tag, fields...)
}

func WarnF(ctx context.Context, tag string, fields ...zap.Field) {
	trace := ctx.Value(consts.TraceKey).(*common.Trace)
	Log.logger.Warn(tag,
		append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
	)
}

func Error(tag string, fields ...zap.Field) {
	Log.logger.Error(tag, fields...)
}

func ErrorF(ctx context.Context, tag string, fields ...zap.Field) {
	trace := ctx.Value(consts.TraceKey).(*common.Trace)
	Log.logger.Error(tag,
		append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
	)
}

func Fatal(tag string, fields ...zap.Field) {
	Log.logger.Fatal(tag, fields...)
}

func FatalF(ctx context.Context, tag string, fields ...zap.Field) {
	trace := ctx.Value(consts.TraceKey).(*common.Trace)
	Log.logger.Fatal(tag,
		append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
	)
}

复制代码

3.3 解决Caller问题

此方式存在个问题,就是打印的caller信息全都是对应日志工具类的代码行,而不是调用处。看了下zap的源码,zap打印时将整个调用的stack链路会存放到内存中,默认打印调用处的caller信息。所以为了解决解决这个问题,需要再初始化zap时额外增加AddCallerSkip跳过指定层级的caller,核心代码如下:

logger = logger.WithOptions(zap.AddCaller(),zap.AddCallerSkip(1))
复制代码

完整的zap工具类如下:

/*
 * Copyright (C) 2021 Baidu, Inc. All Rights Reserved.
 */
package log

import (
	"best-practics/common"
	"best-practics/utils"
	"fmt"
	"os"
	"time"

	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

func InitZap() {
	if ok, _ := utils.PathExists(common.GlobalConfig.Zap.Director); !ok { // 判断是否有Director文件夹
		fmt.Printf("create %v directory\n", common.GlobalConfig.Zap.Director)
		_ = os.Mkdir(common.GlobalConfig.Zap.Director, os.ModePerm)
	}
	// 调试级别
	debugPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
		return lev == zap.DebugLevel
	})
	// 日志级别
	infoPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
		return lev == zap.InfoLevel
	})
	// 警告级别
	warnPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
		return lev == zap.WarnLevel
	})
	// 错误级别
	errorPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
		return lev >= zap.ErrorLevel
	})

	cores := [...]zapcore.Core{
		getEncoderCore(fmt.Sprintf("./%s/server_debug.log", common.GlobalConfig.Zap.Director), debugPriority),
		getEncoderCore(fmt.Sprintf("./%s/server_info.log", common.GlobalConfig.Zap.Director), infoPriority),
		getEncoderCore(fmt.Sprintf("./%s/server_warn.log", common.GlobalConfig.Zap.Director), warnPriority),
		getEncoderCore(fmt.Sprintf("./%s/server_error.log", common.GlobalConfig.Zap.Director), errorPriority),
	}
	logger := zap.New(zapcore.NewTee(cores[:]...), zap.AddCaller(), zap.AddCallerSkip(1))

	common.Logger = logger
}

// getEncoderConfig 获取zapcore.EncoderConfig
func getEncoderConfig() (config zapcore.EncoderConfig) {
	config = zapcore.EncoderConfig{
		MessageKey:     "message",
		LevelKey:       "level",
		TimeKey:        "time",
		NameKey:        "logger",
		CallerKey:      "caller",
		StacktraceKey:  common.GlobalConfig.Zap.StacktraceKey,
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.LowercaseLevelEncoder,
		EncodeTime:     CustomTimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.FullCallerEncoder,
	}
	switch {
	case common.GlobalConfig.Zap.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
		config.EncodeLevel = zapcore.LowercaseLevelEncoder
	case common.GlobalConfig.Zap.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
		config.EncodeLevel = zapcore.LowercaseColorLevelEncoder
	case common.GlobalConfig.Zap.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
		config.EncodeLevel = zapcore.CapitalLevelEncoder
	case common.GlobalConfig.Zap.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
		config.EncodeLevel = zapcore.CapitalColorLevelEncoder
	default:
		config.EncodeLevel = zapcore.LowercaseLevelEncoder
	}
	return config
}

// getEncoder 获取zapcore.Encoder
func getEncoder() zapcore.Encoder {
	if common.GlobalConfig.Zap.Format == "json" {
		return zapcore.NewJSONEncoder(getEncoderConfig())
	}
	return zapcore.NewConsoleEncoder(getEncoderConfig())
}

// getEncoderCore 获取Encoder的zapcore.Core
func getEncoderCore(fileName string, level zapcore.LevelEnabler) (core zapcore.Core) {
	writer := GetWriteSyncer(fileName) // 使用file-rotatelogs进行日志分割
	return zapcore.NewCore(getEncoder(), writer, level)
}

// 自定义日志输出时间格式
func CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
	enc.AppendString(t.Format("2006/01/02 15:04:05"))
}

//@function: GetWriteSyncer
//@description: zap logger中加入file-rotatelogs
//@return: zapcore.WriteSyncer, error
func GetWriteSyncer(file string) zapcore.WriteSyncer {
	// 每小时一个文件
	logf, _ := rotatelogs.New(file +".%Y%m%d%H",
		rotatelogs.WithLinkName(file),
		rotatelogs.WithMaxAge(7*24*time.Hour),
		rotatelogs.WithRotationTime(time.Minute),
	)

	if common.GlobalConfig.Zap.LogInConsole {
		return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(logf))
	}
	return zapcore.AddSync(logf)
}
复制代码

3.4 Gin的middleware中增加trace信息

func SetLoggerMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		uuidStr := strings.ReplaceAll(uuid.New().String(), "-", "")
		path := c.Request.URL.Path
		userId := c.GetInt("user_id")
		ctx := context.WithValue(context.Background(), consts.TraceKey, &common.Trace{TraceId: uuidStr, Caller: path, UserId: userId})
		c.Set(consts.TraceCtx,ctx)

		c.Next()
		cost := time.Since(start)
		common.Logger.Info("_com_request_info",
			zap.Int("Status", c.Writer.Status()),
			zap.String("Method", c.Request.Method),
			zap.String("IP",c.ClientIP()),
			zap.String("Path",path),
			zap.String("TraceId", uuidStr),
			zap.Int("UserId", userId),
			zap.String("query", c.Request.URL.RawQuery),
			zap.String("UserAgent",c.Request.UserAgent()),
			zap.Duration("Cost",cost),
		)
	}
}
复制代码

3.5 Controller层生成trace_context

func main() {
	r := gin.Default()
	r.GET("/hello", func(c *gin.Context) {
        // 3.1中封装的转换方法
        ctx := common.GetTraceCtx(c)
		log.WarnF(ctx,"BookNotFoundError",zap.String("test","666"))
		c.String("hello jianfan.com!")
	})
	r.Run(
}
复制代码

至此我们就完成了在一个gin项目中接入zap作为日志框架,并能够打印完整的链路日志信息。结合前两节我们已经满足了对常见日志框架的需求:

  1. 良好日志写入性能
  2. 支持不同的日志级别。并且可分离成多个日志文件
  3. 多输出 - 同时支持标准输出,文件等
  4. 能够打印基本信息,如调用文件 / 函数名和行号,日志时间等
  5. 可读性与结构化,Json格式或有分隔符,方便后续的日志采集、监控等
  6. 文件切割,可按小时、天进行日志拆分,或者按文件大小
  7. 日志书写友好,支持通过context自动log trace等
  8. 文件定时删除
  9. 开源性,与其他开源框架支持较好

参考资料:

我们下期见,Peace😘

我是简凡,一个励志用最简单的语言,描述最复杂问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么疑惑,欢迎在我的微信公众号中留言,我还可以为你提供以下帮助:

  • 帮助建立自己的知识体系
  • 互联网真实高并发场景实战讲解
  • 不定期分享Golang、Java相关业内的经典场景实践

我的博客:besthpt.github.io/

我的微信:bestbear666

微信公众号:"简凡丶"

文章分类
后端
文章标签