package middleware import ( "strings" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // 成功且延迟低于该阈值的热路径请求不再写访问日志(错误/慢请求仍全量记录)。 const slowRequestThresholdMs = 200 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() if shouldSkipHTTPLog(path, route, status, latencyMs) { 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.Float64("latency_ms", latencyMs), zap.String("client_ip", c.ClientIP()), zap.Int("response_size", c.Writer.Size()), } // 成功响应省略 UA/referer,错误与 4xx/5xx 保留完整现场 if status >= 400 { fields = append(fields, zap.String("user_agent", c.GetHeader("User-Agent")), zap.String("referer", c.GetHeader("Referer")), ) } 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("errors", strings.TrimSpace(c.Errors.String()))) } switch { case status >= 500: logger.Error("http request", fields...) case status >= 400: logger.Warn("http request", fields...) default: logger.Info("http request", fields...) } } } // shouldSkipHTTPLog 判断是否跳过写入:仅跳过「成功 + 非慢请求」的高频热路径。 func shouldSkipHTTPLog(path, route string, status int, latencyMs float64) bool { if status >= 400 { return false } if latencyMs >= slowRequestThresholdMs { return false } return isHotPath(path, route) } func isHotPath(path, route string) bool { if strings.HasSuffix(path, "/unread-count") { return true } if path == "/api/wallet/balance" { return true } if path == "/api/mobile-home-config" { return true } if strings.HasSuffix(path, "/cover") || route == "/api/listings/:id/cover" { return true } if path == "/api/files/object" || path == "/api/admin/files/object" || route == "/api/files/object" || route == "/api/admin/files/object" { return true } return false }