package middleware import ( "net/http" "strings" "time" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // 查询请求默认静默;成功写操作、慢请求、服务端错误、限流和有诊断价值的认证失败会输出。 const slowRequestThresholdMs = 500 func RequestLogger(logger *zap.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() latencyMs := float64(time.Since(start).Microseconds()) / 1000 status := c.Writer.Status() path := c.Request.URL.Path route := c.FullPath() authFailure := meaningfulAuthFailure(c) operation := meaningfulOperation(c.Request.Method, path, status) if shouldSkipHTTPLog(path, route, status, latencyMs) && !authFailure && !operation { return } fields := []zap.Field{ zap.String("request_id", GetRequestID(c)), zap.String("method", c.Request.Method), zap.String("path", path), zap.String("route", route), zap.Int("status", status), zap.String("code", response.CodeFromContext(c)), zap.Float64("duration_ms", latencyMs), zap.String("client_ip", c.ClientIP()), } if userID, ok := c.Get(ContextUserID); ok { fields = append(fields, zap.Any("user_id", userID)) } if adminID, ok := c.Get(ContextAdminID); ok { fields = append(fields, zap.Any("admin_id", adminID)) } if reason, ok := c.Get(ContextAuthFailureReason); ok { fields = append(fields, zap.Any("auth_failure_reason", reason)) } if source, ok := c.Get(ContextAuthTokenSource); ok { fields = append(fields, zap.Any("auth_token_source", source)) } if detail, ok := c.Get(ContextAuthFailureDetail); ok { fields = append(fields, zap.Any("auth_failure_detail", detail)) } if tokenVersion, ok := c.Get(ContextAuthTokenVersion); ok { fields = append(fields, zap.Any("auth_token_version", tokenVersion)) } if currentVersion, ok := c.Get(ContextAuthCurrentVersion); ok { fields = append(fields, zap.Any("auth_current_token_version", currentVersion)) } if len(c.Errors) > 0 { fields = append(fields, zap.String("error", c.Errors.Last().Err.Error()), zap.Int("error_count", len(c.Errors)), ) } if stack, ok := c.Get(contextPanicStack); ok { fields = append(fields, zap.Any("stack", stack)) } switch { case status >= 500: logger.Error("HTTP 请求失败", fields...) case status == http.StatusTooManyRequests: logger.Warn("HTTP 请求被限流", fields...) case authFailure: logger.Warn("后台认证失败", fields...) case latencyMs >= slowRequestThresholdMs: logger.Warn("HTTP 慢请求", fields...) case operation: logger.Info("HTTP 操作完成", fields...) } } } func meaningfulOperation(method, path string, status int) bool { if status < 200 || status >= 400 { return false } // 支付回调已有更完整的业务结果日志,不再重复记录一条访问日志。 if isPaymentNotifyPath(path) { return false } switch method { case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete: return true default: return false } } func isPaymentNotifyPath(path string) bool { return (strings.Contains(path, "/payment/") || strings.Contains(path, "/payments/")) && strings.HasSuffix(strings.TrimSuffix(path, "/"), "/notify") } // shouldSkipHTTPLog 判断是否为无需记录的普通请求。 func shouldSkipHTTPLog(_, _ string, status int, latencyMs float64) bool { if status >= 500 || status == http.StatusTooManyRequests { return false } if latencyMs >= slowRequestThresholdMs { return false } return true } func meaningfulAuthFailure(c *gin.Context) bool { value, ok := c.Get(ContextAuthFailureReason) if !ok { return false } reason, _ := value.(string) return reason != "" && reason != "missing" }