veza/veza-backend-api/internal/middleware/error_handler.go
2025-12-12 21:34:34 -05:00

228 lines
7 KiB
Go

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
}
}