veza/veza-backend-api/internal/metrics/aggregation.go
2025-12-03 20:29:37 +01:00

243 lines
6.5 KiB
Go

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