feat(middleware): 新建请求日志与 Panic 恢复中间件
- Logger 中间件:生成 request_id(注入 X-Request-ID header),记录 method/path/status/latency/client_ip - Recovery 中间件:自定义 panic 恢复,记录 request 上下文和堆栈
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
package mildware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gen2d/internal/logger"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger 返回 HTTP 请求日志中间件。
|
||||||
|
// 为每个请求生成 request_id,记录 method、path、status、latency、client_ip。
|
||||||
|
func Logger() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
requestID := generateRequestID()
|
||||||
|
|
||||||
|
c.Set("request_id", requestID)
|
||||||
|
ctx, _ := logger.WithRequestID(c.Request.Context(), requestID)
|
||||||
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
|
||||||
|
c.Header("X-Request-ID", requestID)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
latency := time.Since(start)
|
||||||
|
status := c.Writer.Status()
|
||||||
|
|
||||||
|
l := logger.FromCtx(ctx)
|
||||||
|
attrs := []slog.Attr{
|
||||||
|
slog.String("method", c.Request.Method),
|
||||||
|
slog.String("path", c.Request.URL.Path),
|
||||||
|
slog.Int("status", status),
|
||||||
|
slog.Duration("latency", latency),
|
||||||
|
slog.String("client_ip", c.ClientIP()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if status >= 500 {
|
||||||
|
l.LogAttrs(ctx, slog.LevelError, "request completed", attrs...)
|
||||||
|
} else if status >= 400 {
|
||||||
|
l.LogAttrs(ctx, slog.LevelWarn, "request completed", attrs...)
|
||||||
|
} else {
|
||||||
|
l.LogAttrs(ctx, slog.LevelInfo, "request completed", attrs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recovery 返回自定义 panic 恢复中间件。
|
||||||
|
// panic 时记录完整的 request 上下文和堆栈,返回 500。
|
||||||
|
func Recovery() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
l := logger.FromCtx(ctx)
|
||||||
|
l.Error("panic recovered",
|
||||||
|
"error", fmt.Sprintf("%v", r),
|
||||||
|
"method", c.Request.Method,
|
||||||
|
"path", c.Request.URL.Path,
|
||||||
|
"request_id", c.GetString("request_id"),
|
||||||
|
"stack", string(debug.Stack()),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError,
|
||||||
|
gin.H{"code": http.StatusInternalServerError, "message": "服务器内部错误"})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRequestID() string {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
return fmt.Sprintf("%d-%x", time.Now().UnixMilli(), b)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user