Gin 中间件实现Panic拦截Recovery并飞书发送错堆栈信息

166 阅读1分钟

构建中间件,当发生panic错误时拦截panic并Recovery,飞书通知堆栈信息

包括 httpRequest、stack错误堆栈信息

Log middleware


package middleware

import (
    "fmt"

    "net"
    "net/http/httputil"
    "os"
    "runtime/debug"
    "strings"

    "net/http"

    "time"

    "github.com/gin-gonic/gin"
    "xxxx.com/infra/common/util/feishu"

    "xxxx.com/infra/saplat/initialize"

    "go.uber.org/zap"
)

const (
    FSBot = "https://open.feishu.cn/open-apis/bot/v2/hook/xxxx" 
)

var (
    AccessLogger = initialize.AccessLogger
    ErrorLogger  = initialize.ErrorLogger
)

// 记录access log的中间件
func AccessLoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {

        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery
        clientId := c.ClientIP()
        method := c.Request.Method
        userAgent := c.Request.UserAgent()

        c.Next()

        latency := time.Since(start)

        // 记录access log
        AccessLogger.Info(path,
            zap.Int("status", c.Writer.Status()),
            zap.String("method", method),
            zap.String("path", path),
            zap.String("query", query),
            zap.String("client_ip", clientId),
            zap.String("user_agent", userAgent),
            zap.Duration("latency", latency),
        )

        // 错误处理和日志记录
        if len(c.Errors) > 0 {
            ErrorLogger.Error("Encountered error",
                zap.String("path", path),
                zap.String("client_ip", clientId),
                zap.Error(c.Errors.ByType(gin.ErrorTypePrivate).Last().Err), // 获取最后一个错误
                zap.Stack("stack"),
            )
        }
    }

}


func RecoveryLoggerMiddleware(logger *zap.Logger, stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {

				// 打印错误堆栈信息到控制台
				fmt.Printf("panic: %v\n", err)
				debug.PrintStack()

				// 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
						}
					}
				}

				var errMsg string
				if str, ok := err.(string); ok {
					errMsg = str
				}
				errMsg = time.Now().Local().String() + "\n" + errMsg + "\n"
				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				errMsg = errMsg + "\n" + string(httpRequest) + "\n"
				if brokenPipe {
					logger.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					v4.SendTextMsgByAppID("chat_id", FSBot, "text", errMsg)
					// 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 and Panic Stack]",
						zap.Time("time", time.Now()),
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.Stack("stack"), // 打印出错时的堆栈跟踪
					)
					errMsg = errMsg + "\n" + zap.Stack("stack").String + "\n"
					v4.SendTextMsgByAppID("chat_id", FSBot, "text", errMsg)
				} else {
					logger.Error("[Recovery from panic]",
						zap.Time("time", time.Now()),
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					v4.SendTextMsgByAppID("chat_id", FSBot, "text", errMsg)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

引入middleware

r := gin.Default()

    /*
        替代gin.Recovery() 实现panic拦截以及日志记录
    */
    r.Use(middleware.AccessLoggerMiddleware(), middleware.RecoveryLoggerMiddleware(ErrorLogger, true))