veza/veza-backend-api/internal/middleware/error_handler.go

286 lines
8.9 KiB
Go

package middleware
import (
"net/http"
"runtime/debug"
"time"
"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...)
// INT-006: Standardize error response format using APIResponse
errorData := struct {
Code int `json:"code"`
Message string `json:"message"`
Details []errors.ErrorDetail `json:"details,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp string `json:"timestamp"`
Context map[string]interface{} `json:"context,omitempty"`
}{
Code: int(appErr.Code),
Message: appErr.Message,
Details: appErr.Details,
RequestID: c.GetString("request_id"),
Timestamp: time.Now().UTC().Format(time.RFC3339),
Context: appErr.Context,
}
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error interface{} `json:"error,omitempty"`
}
c.JSON(httpStatus, APIResponse{
Success: false,
Data: nil,
Error: errorData,
})
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...)
// INT-006: Standardize error response format using APIResponse
errorData := struct {
Code int `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id,omitempty"`
Timestamp string `json:"timestamp"`
}{
Code: int(errors.ErrCodeNotFound),
Message: "Resource not found",
RequestID: c.GetString("request_id"),
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error interface{} `json:"error,omitempty"`
}
c.JSON(http.StatusNotFound, APIResponse{
Success: false,
Data: nil,
Error: errorData,
})
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...)
// INT-006: Standardize error response format using APIResponse
errorData := struct {
Code int `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id,omitempty"`
Timestamp string `json:"timestamp"`
}{
Code: int(errors.ErrCodeInternal),
Message: "Internal server error",
RequestID: c.GetString("request_id"),
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error interface{} `json:"error,omitempty"`
}
c.JSON(http.StatusInternalServerError, APIResponse{
Success: false,
Data: nil,
Error: errorData,
})
}
}
}
// 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
}
}