228 lines
7 KiB
Go
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
|
|
}
|
|
}
|