中间件配置 原创
在已完成的开发工作中,我们使用了默认的Logger
和Recovery
中间件,如下:
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 "中间件配置"