package metrics import ( "sync" "time" "veza-backend-api/internal/errors" ) // TimeWindow représente une fenêtre de temps avec des métriques agrégées type TimeWindow struct { Start time.Time `json:"start"` End time.Time `json:"end"` Errors int64 `json:"errors"` Requests int64 `json:"requests"` ErrorsByCode map[errors.ErrorCode]int64 `json:"errors_by_code"` ErrorsByHTTPStatus map[int]int64 `json:"errors_by_http_status"` } // AggregatedMetrics gère l'agrégation des métriques sur des fenêtres de temps type AggregatedMetrics struct { mu sync.RWMutex windows map[string][]TimeWindow // key: "1m", "5m", "1h" // Configuration des fenêtres en secondes windowSizes map[string]time.Duration maxWindows map[string]int // Nombre max de fenêtres à garder par type } // NewAggregatedMetrics crée une nouvelle instance de AggregatedMetrics func NewAggregatedMetrics() *AggregatedMetrics { agg := &AggregatedMetrics{ windows: make(map[string][]TimeWindow), windowSizes: map[string]time.Duration{ "1m": 1 * time.Minute, "5m": 5 * time.Minute, "1h": 1 * time.Hour, }, maxWindows: map[string]int{ "1m": 60, // Garder 60 fenêtres de 1 minute = 1 heure "5m": 12, // Garder 12 fenêtres de 5 minutes = 1 heure "1h": 24, // Garder 24 fenêtres de 1 heure = 24 heures }, } // Démarrer la routine de nettoyage go agg.cleanupRoutine() return agg } // AddError enregistre une erreur dans les fenêtres d'agrégation func (a *AggregatedMetrics) AddError(windowType string, code errors.ErrorCode, httpStatus int) { a.mu.Lock() defer a.mu.Unlock() now := time.Now() // Initialiser la fenêtre si elle n'existe pas if _, exists := a.windows[windowType]; !exists { a.windows[windowType] = []TimeWindow{} } windowSize, ok := a.windowSizes[windowType] if !ok { // Fenêtre non supportée return } // Trouver ou créer la fenêtre active windowStart := now.Truncate(windowSize) windowEnd := windowStart.Add(windowSize) // Chercher la fenêtre active found := false for i := range a.windows[windowType] { if a.windows[windowType][i].Start.Equal(windowStart) { // Fenêtre existante - mettre à jour a.windows[windowType][i].Errors++ a.windows[windowType][i].ErrorsByCode[code]++ a.windows[windowType][i].ErrorsByHTTPStatus[httpStatus]++ found = true break } } if !found { // Créer une nouvelle fenêtre newWindow := TimeWindow{ Start: windowStart, End: windowEnd, Errors: 1, Requests: 0, ErrorsByCode: make(map[errors.ErrorCode]int64), ErrorsByHTTPStatus: make(map[int]int64), } newWindow.ErrorsByCode[code] = 1 newWindow.ErrorsByHTTPStatus[httpStatus] = 1 a.windows[windowType] = append(a.windows[windowType], newWindow) } // Nettoyer les anciennes fenêtres (garder seulement les plus récentes) a.cleanupWindows(windowType) } // AddRequest enregistre une requête dans les fenêtres d'agrégation func (a *AggregatedMetrics) AddRequest(windowType string) { a.mu.Lock() defer a.mu.Unlock() now := time.Now() // Initialiser la fenêtre si elle n'existe pas if _, exists := a.windows[windowType]; !exists { a.windows[windowType] = []TimeWindow{} } windowSize, ok := a.windowSizes[windowType] if !ok { return } // Trouver ou créer la fenêtre active windowStart := now.Truncate(windowSize) // Chercher la fenêtre active found := false for i := range a.windows[windowType] { if a.windows[windowType][i].Start.Equal(windowStart) { a.windows[windowType][i].Requests++ found = true break } } if !found { // Créer une nouvelle fenêtre newWindow := TimeWindow{ Start: windowStart, End: windowStart.Add(windowSize), Errors: 0, Requests: 1, ErrorsByCode: make(map[errors.ErrorCode]int64), ErrorsByHTTPStatus: make(map[int]int64), } a.windows[windowType] = append(a.windows[windowType], newWindow) } // Nettoyer les anciennes fenêtres a.cleanupWindows(windowType) } // GetAggregated retourne les métriques agrégées pour un type de fenêtre func (a *AggregatedMetrics) GetAggregated(windowType string) []TimeWindow { a.mu.RLock() defer a.mu.RUnlock() if windows, exists := a.windows[windowType]; exists { // Retourner une copie pour éviter les modifications concurrentes result := make([]TimeWindow, len(windows)) for i, w := range windows { result[i] = w // Copier les maps result[i].ErrorsByCode = make(map[errors.ErrorCode]int64) result[i].ErrorsByHTTPStatus = make(map[int]int64) for k, v := range w.ErrorsByCode { result[i].ErrorsByCode[k] = v } for k, v := range w.ErrorsByHTTPStatus { result[i].ErrorsByHTTPStatus[k] = v } } return result } return []TimeWindow{} } // GetAllAggregated retourne toutes les métriques agrégées func (a *AggregatedMetrics) GetAllAggregated() map[string][]TimeWindow { a.mu.RLock() defer a.mu.RUnlock() result := make(map[string][]TimeWindow) for windowType := range a.windows { result[windowType] = a.GetAggregated(windowType) } return result } // cleanupWindows nettoie les anciennes fenêtres pour un type donné func (a *AggregatedMetrics) cleanupWindows(windowType string) { max, ok := a.maxWindows[windowType] if !ok { return } if len(a.windows[windowType]) <= max { return } // Garder seulement les fenêtres les plus récentes windows := a.windows[windowType] // Trier par date (les plus récentes en premier) // Les fenêtres sont normalement déjà ordonnées, mais on s'assure // On garde les max dernières if len(windows) > max { startIdx := len(windows) - max a.windows[windowType] = windows[startIdx:] } } // cleanupRoutine nettoie périodiquement les anciennes fenêtres func (a *AggregatedMetrics) cleanupRoutine() { ticker := time.NewTicker(1 * time.Minute) // Nettoyer chaque minute defer ticker.Stop() for range ticker.C { a.mu.Lock() now := time.Now() // Nettoyer les fenêtres expirées pour chaque type for windowType, windows := range a.windows { windowSize := a.windowSizes[windowType] maxAge := windowSize * time.Duration(a.maxWindows[windowType]) validWindows := []TimeWindow{} for _, w := range windows { // Garder les fenêtres qui ne sont pas trop anciennes if now.Sub(w.End) < maxAge { validWindows = append(validWindows, w) } } a.windows[windowType] = validWindows } a.mu.Unlock() } }