Skip to content

中间件配置 原创

在已完成的开发工作中,我们使用了默认的LoggerRecovery中间件,如下:

go
// 注册中间件
	g.Use(gin.Logger())
	g.Use(gin.Recovery())

这两个中间件会输出一些访问和异常记录,但是,并不会输出比较详细的信息,比如是哪个接口,入参记录是什么,因此无法使开发人员快速定位到问题。

为此,我们对中间件进行自定义配置。

1、访问日志记录

当出现问题时,我们常常需要查看日志,除了查看错误日志、业务日志,还有一个很重要的日志类别,那就是访问日志。从功能上讲,它会记录每一次请求的请求方法、方法调用开始时间、方法调用结束时间、方法响应结果和方法响应结果状态码。除此之外,它还会记录 RequestId、TraceId、SpanId 等附加属性,以达到日志链路追踪的效果。

internal/app/middlewares 中新建 logger.go 文件,写入以下内容:

go
package middlewares

import (
	"bytes"
	"fmt"
	"io"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/joker-bai/hawkeye/global"
	"github.com/spf13/cast"
	"go.uber.org/zap"
)

type responseBodyWriter struct {
	gin.ResponseWriter
	body *bytes.Buffer
}

func (r responseBodyWriter) Write(b []byte) (int, error) {
	r.body.Write(b)
	return r.ResponseWriter.Write(b)
}

// Logger 记录请求日志
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {

		// 获取 response 内容
		w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
		c.Writer = w

		// 获取请求数据
		var requestBody []byte
		if c.Request.Body != nil {
			// c.Request.Body 是一个 buffer 对象,只能读取一次
			requestBody, _ = io.ReadAll(c.Request.Body)
			// 读取后,重新赋值 c.Request.Body ,以供后续的其他操作
			c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
		}

		// 设置开始时间
		start := time.Now()
		c.Next()

		// 开始记录日志的逻辑
		cost := time.Since(start)
		responStatus := c.Writer.Status()

		logFields := []zap.Field{
			zap.Int("status", responStatus),
			zap.String("request", c.Request.Method+" "+c.Request.URL.String()),
			zap.String("query", c.Request.URL.RawQuery),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.String("time", fmt.Sprintf("%.3fms", float64(cost.Nanoseconds())/1e6)),
		}
		if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "DELETE" {
			// 请求的内容
			logFields = append(logFields, zap.String("Request Body", string(requestBody)))

			// 响应的内容
			logFields = append(logFields, zap.String("Response Body", w.body.String()))
		}

		if responStatus > 400 && responStatus <= 499 {
			// 除了 StatusBadRequest 以外,warning 提示一下,常见的有 403 404,开发时都要注意
			global.Log.Warn("HTTP Warning "+cast.ToString(responStatus), logFields...)
		} else if responStatus >= 500 && responStatus <= 599 {
			// 除了内部错误,记录 error
			global.Log.Error("HTTP Error "+cast.ToString(responStatus), logFields...)
		} else {
			global.Log.Debug("HTTP Access Log", logFields...)
		}
	}
}

然后修改 initialize/server.go 中代码,使用中间件:

go
func (s *Engine) injectMiddlewares() {
	g := gin.New()
	defer func() {
		s.Engine = g
	}()

	if s.Mode == gin.TestMode {
		return
	}

	// 注册中间件
	g.Use(middlewares.Logger())
	g.Use(gin.Recovery())
}

现在我们来测试一下,使用 apifox 发送一个请求,日志输出如下:

json
{
  "level": "debug",
  "ts": "2023-03-20T15:08:32",
  "caller": "middleware/logger.go:74",
  "msg": "HTTP Access Log",
  "status": 200,
  "request": "GET /api/hello",
  "query": "",
  "ip": "127.0.0.1",
  "user-agent": "apifox/1.0.0 (https://www.apifox.cn)",
  "errors": "",
  "time": "0.000ms"
}

2、异常捕获处理

Go 语言中 panic 关键字主要用于主动抛出异常。而 recover 则使捕获异常,让程序回到正常状态。Gin 内置了 Recovery 中间件,但是我们现在希望其用 zap 来记录,所以对其进行改造。

internal/app/middlewares 中新建 recovery.go 文件,写入以下内容:

go
package middleware

import (
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/joker-bai/hawkeye/global"
	"go.uber.org/zap"
)

// Recovery 使用 zap.Error() 来记录 Panic 和 call stack
func Recovery() gin.HandlerFunc {

	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {

				// 获取用户的请求信息
				httpRequest, _ := httputil.DumpRequest(c.Request, true)

				// 链接中断,客户端中断连接为正常行为,不需要记录堆栈信息
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						errStr := strings.ToLower(se.Error())
						if strings.Contains(errStr, "broken pipe") || strings.Contains(errStr, "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				// 链接中断的情况
				if brokenPipe {
					global.Log.Error(c.Request.URL.Path,
						zap.Time("time", time.Now()),
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					c.Error(err.(error))
					c.Abort()
					// 链接已断开,无法写状态码
					return
				}

				// 如果不是链接中断,就开始记录堆栈信息
				global.Log.Error("recovery from panic",
					zap.Time("time", time.Now()),               // 记录时间
					zap.Any("error", err),                      // 记录错误信息
					zap.String("request", string(httpRequest)), // 请求信息
					zap.Stack("stacktrace"),                    // 调用堆栈信息
				)

				// 返回 500 状态码
				c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
					"message": "服务器内部错误,请稍后再试",
				})
			}
		}()
		c.Next()
	}
}

然后修改 initialize/server.go 中代码,使用中间件:

go
func (s *Engine) injectMiddlewares() {
	g := gin.New()
	defer func() {
		s.Engine = g
	}()

	if s.Mode == gin.TestMode {
		return
	}

	// 注册中间件
	g.Use(middlewares.Logger())
	g.Use(middlewares.Recovery())
}

3、代码标记

本节开发完成后,记得给代码打标记,如下:

go
$ git add .
$ git commit -m "中间件配置"
最近更新