package middleware import ( "net/http" "runtime/debug" "veza-backend-api/internal/errors" errorMetricsPkg "veza-backend-api/internal/metrics" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/gorm" ) // ErrorHandler middleware pour gérer toutes les erreurs de manière standardisée func ErrorHandler(logger *zap.Logger, errorMetrics *errorMetricsPkg.ErrorMetrics, includeStackTrace bool) gin.HandlerFunc { return func(c *gin.Context) { c.Next() // Traiter les erreurs stockées dans le contexte if len(c.Errors) > 0 { err := c.Errors.Last().Err // Vérifier si c'est une AppError personnalisée if appErr, ok := err.(*errors.AppError); ok { // Enrichir l'erreur avec le contexte de la requête enrichErrorWithContext(c, appErr) httpStatus := mapErrorCodeToHTTPStatus(appErr.Code) // Enregistrer l'erreur dans les métriques (T0020) if errorMetrics != nil { errorMetrics.RecordError(appErr.Code, httpStatus) } // Enregistrer l'erreur dans Prometheus (T0021) errorMetricsPkg.RecordErrorPrometheus(appErr.Code, httpStatus) // Logger structuré avec contexte complet (T0028) logFields := []zap.Field{ zap.Int("code", int(appErr.Code)), zap.String("message", appErr.Message), zap.Int("http_status", httpStatus), } // Ajouter les champs de contexte au logger si disponibles if appErr.Context != nil { if requestID, ok := appErr.Context["request_id"].(string); ok { logFields = append(logFields, zap.String("request_id", requestID)) } if userID, ok := appErr.Context["user_id"]; ok { logFields = append(logFields, zap.Any("user_id", userID)) } } // Ajouter trace_id et span_id si disponibles (T0025) if traceID := GetTraceID(c); traceID != "" { logFields = append(logFields, zap.String("trace_id", traceID)) } if spanID := GetSpanID(c); spanID != "" { logFields = append(logFields, zap.String("span_id", spanID)) } // Ajouter l'erreur causale si présente if appErr.Err != nil { logFields = append(logFields, zap.Error(appErr.Err)) } // Ajouter les détails de validation si présents if len(appErr.Details) > 0 { logFields = append(logFields, zap.Any("details", appErr.Details)) } // Logger au niveau ERROR avec format JSON structuré logger.Error("Application error", logFields...) c.JSON(httpStatus, gin.H{ "error": gin.H{ "code": appErr.Code, "message": appErr.Message, "details": appErr.Details, "context": appErr.Context, }, }) return } // Vérifier si c'est une erreur GORM if err == gorm.ErrRecordNotFound { // Enregistrer l'erreur dans les métriques (T0020) if errorMetrics != nil { errorMetrics.RecordError(errors.ErrCodeNotFound, http.StatusNotFound) } // Enregistrer l'erreur dans Prometheus (T0021) errorMetricsPkg.RecordErrorPrometheus(errors.ErrCodeNotFound, http.StatusNotFound) // Logger structuré avec contexte logFields := []zap.Field{ zap.Int("code", int(errors.ErrCodeNotFound)), zap.String("message", "Resource not found"), zap.Int("http_status", http.StatusNotFound), zap.Error(err), } // Ajouter request_id si disponible if requestID, exists := c.Get("request_id"); exists { if requestIDStr, ok := requestID.(string); ok { logFields = append(logFields, zap.String("request_id", requestIDStr)) } } // Ajouter trace_id et span_id si disponibles (T0025) if traceID := GetTraceID(c); traceID != "" { logFields = append(logFields, zap.String("trace_id", traceID)) } if spanID := GetSpanID(c); spanID != "" { logFields = append(logFields, zap.String("span_id", spanID)) } logger.Warn("Record not found", logFields...) c.JSON(http.StatusNotFound, gin.H{ "error": gin.H{ "code": errors.ErrCodeNotFound, "message": "Resource not found", }, }) return } // Erreur générique - logging structuré avec stack trace (T0028) // Enregistrer l'erreur dans les métriques (T0020) if errorMetrics != nil { errorMetrics.RecordError(errors.ErrCodeInternal, http.StatusInternalServerError) } // Enregistrer l'erreur dans Prometheus (T0021) errorMetricsPkg.RecordErrorPrometheus(errors.ErrCodeInternal, http.StatusInternalServerError) // Logger structuré avec contexte complet et stack trace logFields := []zap.Field{ zap.Int("code", int(errors.ErrCodeInternal)), zap.String("message", "Internal server error"), zap.Int("http_status", http.StatusInternalServerError), zap.Error(err), } // Ajouter stack trace uniquement si configuré (MOD-P1-006) if includeStackTrace { logFields = append(logFields, zap.ByteString("stack_trace", debug.Stack())) } // Ajouter request_id si disponible if requestID, exists := c.Get("request_id"); exists { if requestIDStr, ok := requestID.(string); ok { logFields = append(logFields, zap.String("request_id", requestIDStr)) } } // Ajouter user_id si disponible if userID, exists := c.Get("user_id"); exists { logFields = append(logFields, zap.Any("user_id", userID)) } // Ajouter trace_id et span_id si disponibles (T0025) if traceID := GetTraceID(c); traceID != "" { logFields = append(logFields, zap.String("trace_id", traceID)) } if spanID := GetSpanID(c); spanID != "" { logFields = append(logFields, zap.String("span_id", spanID)) } // Logger au niveau ERROR avec format JSON structuré logger.Error("Internal server error", logFields...) c.JSON(http.StatusInternalServerError, gin.H{ "error": gin.H{ "code": errors.ErrCodeInternal, "message": "Internal server error", }, }) } } } // enrichErrorWithContext enrichit une AppError avec le contexte de la requête (request_id, user_id) func enrichErrorWithContext(c *gin.Context, appErr *errors.AppError) { if appErr.Context == nil { appErr.Context = make(map[string]interface{}) } // Ajouter le request_id depuis le contexte Gin if requestID, exists := c.Get("request_id"); exists { if requestIDStr, ok := requestID.(string); ok { appErr.Context["request_id"] = requestIDStr } } // Ajouter le user_id depuis le contexte Gin si disponible if userID, exists := c.Get("user_id"); exists { appErr.Context["user_id"] = userID } } // mapErrorCodeToHTTPStatus convertit un code d'erreur en status HTTP func mapErrorCodeToHTTPStatus(code errors.ErrorCode) int { switch { case code >= 1000 && code < 2000: if code == errors.ErrCodeForbidden { return http.StatusForbidden } return http.StatusUnauthorized case code >= 2000 && code < 3000: return http.StatusBadRequest case code >= 3000 && code < 4000: if code == errors.ErrCodeNotFound { return http.StatusNotFound } if code == errors.ErrCodeConflict || code == errors.ErrCodeAlreadyExists { return http.StatusConflict } return http.StatusBadRequest case code >= 5000 && code < 6000: return http.StatusTooManyRequests default: return http.StatusInternalServerError } }