package middleware import ( "log/slog" "net/http" "net/url" "strings" "time" "github.com/gin-gonic/gin" ) // sanitizeQuery 对 query 中的敏感参数值脱敏,避免日志泄露 token/code/密钥等。 func sanitizeQuery(query string) string { if query == "" { return "" } values, err := url.ParseQuery(query) if err != nil { return "***" } sensitive := map[string]bool{ "token": true, "code": true, "secret": true, "password": true, "access_token": true, "refresh_token": true, "key": true, } for k := range values { if sensitive[strings.ToLower(k)] { values.Set(k, "***") } } return values.Encode() } // maxUserAgentLen user_agent 日志字段最大长度:超出截断,避免异常长 UA 撑大日志体积。 const maxUserAgentLen = 256 // truncateUserAgent 截断超长 User-Agent(仅用于日志展示)。 func truncateUserAgent(ua string) string { if len(ua) <= maxUserAgentLen { return ua } return ua[:maxUserAgentLen] + "...(truncated)" } func GinSlogMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path query := sanitizeQuery(c.Request.URL.RawQuery) // 处理请求 c.Next() // 请求结束后按状态码分级打日志:5xx=Error、4xx(除 401)=Warn、其余=Info。 ctx := c.Request.Context() status := c.Writer.Status() attrs := []any{ "request_id", GetRequestID(ctx), "method", c.Request.Method, "path", path, "query", query, "status", status, "ip", c.ClientIP(), "latency", time.Since(start).String(), "user_agent", truncateUserAgent(c.Request.UserAgent()), } switch { case status >= http.StatusInternalServerError: slog.ErrorContext(ctx, "GIN_REQUEST", attrs...) case status >= http.StatusBadRequest && status != http.StatusUnauthorized: slog.WarnContext(ctx, "GIN_REQUEST", attrs...) default: slog.InfoContext(ctx, "GIN_REQUEST", attrs...) } } }