package main import ( "context" "crypto/tls" "encoding/csv" "encoding/json" "flag" "fmt" "io" "log" "math" "net/http" "os" "sort" "strings" "sync" "sync/atomic" "time" ) type TimeSeriesMetric struct { Timestamp time.Time `json:"timestamp"` Requests int64 `json:"requests"` Success int64 `json:"success"` Errors int64 `json:"errors"` AvgLatency time.Duration `json:"avg_latency_ms"` MinLatency time.Duration `json:"min_latency_ms"` MaxLatency time.Duration `json:"max_latency_ms"` RPS float64 `json:"rps"` ErrorRate float64 `json:"error_rate_percent"` } type AggregatedMetrics struct { StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` TotalRequests int64 `json:"total_requests"` SuccessRequests int64 `json:"success_requests"` ErrorRequests int64 `json:"error_requests"` ErrorRate float64 `json:"error_rate_percent"` AvgRPS float64 `json:"avg_rps"` LatencyAvg time.Duration `json:"latency_avg_ms"` LatencyMedian time.Duration `json:"latency_median_ms"` LatencyP95 time.Duration `json:"latency_p95_ms"` LatencyP99 time.Duration `json:"latency_p99_ms"` LatencyMin time.Duration `json:"latency_min_ms"` LatencyMax time.Duration `json:"latency_max_ms"` ErrorsByType map[string]int64 `json:"errors_by_type"` TimeSeries []TimeSeriesMetric `json:"time_series"` } type Metrics struct { totalReqs int64 successReqs int64 errorReqs int64 latencies []time.Duration errors map[string]int64 timeSeries []TimeSeriesMetric currentWindow *WindowMetrics mu sync.RWMutex // Changé de Mutex à RWMutex startTime time.Time endTime time.Time windowSize time.Duration } type WindowMetrics struct { startTime time.Time requests int64 success int64 errors int64 latencies []time.Duration minLatency time.Duration maxLatency time.Duration mu sync.Mutex // Mutex spécifique pour la fenêtre } type Result struct { latency time.Duration err error status int } func NewMetrics(windowSize time.Duration) *Metrics { return &Metrics{ errors: make(map[string]int64), timeSeries: make([]TimeSeriesMetric, 0), windowSize: windowSize, currentWindow: &WindowMetrics{ startTime: time.Now(), latencies: make([]time.Duration, 0), minLatency: time.Hour, maxLatency: 0, }, } } func (m *Metrics) AddResult(latency time.Duration, err error, status int) { atomic.AddInt64(&m.totalReqs, 1) // Gestion des erreurs if err != nil || status >= 400 { atomic.AddInt64(&m.errorReqs, 1) m.mu.Lock() if err != nil { m.errors[err.Error()]++ } else { m.errors[fmt.Sprintf("HTTP_%d", status)]++ } m.mu.Unlock() } else { atomic.AddInt64(&m.successReqs, 1) m.mu.Lock() m.latencies = append(m.latencies, latency) m.mu.Unlock() } // Mise à jour de la fenêtre courante avec son propre mutex m.currentWindow.mu.Lock() m.currentWindow.requests++ if err == nil && status < 400 { m.currentWindow.success++ m.currentWindow.latencies = append(m.currentWindow.latencies, latency) if latency < m.currentWindow.minLatency { m.currentWindow.minLatency = latency } if latency > m.currentWindow.maxLatency { m.currentWindow.maxLatency = latency } } else { m.currentWindow.errors++ } m.currentWindow.mu.Unlock() // Vérifier si on doit fermer la fenêtre now := time.Now() if now.Sub(m.currentWindow.startTime) >= m.windowSize { m.closeCurrentWindow(now) } } func (m *Metrics) closeCurrentWindow(now time.Time) { m.currentWindow.mu.Lock() defer m.currentWindow.mu.Unlock() if m.currentWindow.requests == 0 { // Réinitialiser la fenêtre même si vide m.currentWindow.startTime = now return } // Calculer la latence moyenne de la fenêtre var avgLatency time.Duration if len(m.currentWindow.latencies) > 0 { var sum int64 = 0 for _, lat := range m.currentWindow.latencies { sum += int64(lat) } avgLatency = time.Duration(sum / int64(len(m.currentWindow.latencies))) } // Calculer la durée réelle (minimum 1ms pour éviter division par zéro) duration := now.Sub(m.currentWindow.startTime) if duration <= 0 { duration = 1 * time.Millisecond } // Calculer le RPS avec vérification des bornes rps := float64(m.currentWindow.requests) / duration.Seconds() // Vérifier les valeurs aberrantes (NaN, Inf, négatives) if math.IsNaN(rps) || math.IsInf(rps, 0) || rps < 0 { log.Printf("Warning: Invalid RPS value detected: %f, using 0", rps) rps = 0 } // Calculer le taux d'erreur errorRate := 0.0 if m.currentWindow.requests > 0 { errorRate = float64(m.currentWindow.errors) / float64(m.currentWindow.requests) * 100 } // Gérer le cas où minLatency n'a pas été mis à jour minLatency := m.currentWindow.minLatency if minLatency == time.Hour { minLatency = 0 } metric := TimeSeriesMetric{ Timestamp: m.currentWindow.startTime, Requests: m.currentWindow.requests, Success: m.currentWindow.success, Errors: m.currentWindow.errors, AvgLatency: avgLatency, MinLatency: minLatency, MaxLatency: m.currentWindow.maxLatency, RPS: rps, ErrorRate: errorRate, } m.mu.Lock() m.timeSeries = append(m.timeSeries, metric) m.mu.Unlock() // Réinitialiser la fenêtre m.currentWindow = &WindowMetrics{ startTime: now, latencies: make([]time.Duration, 0), minLatency: time.Hour, maxLatency: 0, } } func (m *Metrics) SetEndTime() { m.endTime = time.Now() m.closeCurrentWindow(m.endTime) } func (m *Metrics) GetAggregatedMetrics() *AggregatedMetrics { m.mu.RLock() defer m.mu.RUnlock() total := atomic.LoadInt64(&m.totalReqs) success := atomic.LoadInt64(&m.successReqs) errors := atomic.LoadInt64(&m.errorReqs) duration := m.endTime.Sub(m.startTime) if duration <= 0 { duration = 1 * time.Millisecond } errorRate := 0.0 if total > 0 { errorRate = float64(errors) / float64(total) * 100 } avgRPS := float64(total) / duration.Seconds() if math.IsNaN(avgRPS) || math.IsInf(avgRPS, 0) { avgRPS = 0 } // Calculer les percentiles var avgLatency, median, p95, p99, minLat, maxLat time.Duration if len(m.latencies) > 0 { sorted := make([]time.Duration, len(m.latencies)) copy(sorted, m.latencies) sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) // Éviter les overflow dans la somme var sum int64 = 0 for _, lat := range sorted { sum += int64(lat) } avgLatency = time.Duration(sum / int64(len(sorted))) median = sorted[len(sorted)/2] p95 = percentile(sorted, 0.95) p99 = percentile(sorted, 0.99) minLat = sorted[0] maxLat = sorted[len(sorted)-1] } return &AggregatedMetrics{ StartTime: m.startTime, EndTime: m.endTime, TotalRequests: total, SuccessRequests: success, ErrorRequests: errors, ErrorRate: errorRate, AvgRPS: avgRPS, LatencyAvg: avgLatency, LatencyMedian: median, LatencyP95: p95, LatencyP99: p99, LatencyMin: minLat, LatencyMax: maxLat, ErrorsByType: m.errors, TimeSeries: m.timeSeries, } } func percentile(data []time.Duration, p float64) time.Duration { if len(data) == 0 { return 0 } idx := int(float64(len(data)) * p) if idx >= len(data) { idx = len(data) - 1 } if idx < 0 { idx = 0 } return data[idx] } // Export CSV avec validation func (m *AggregatedMetrics) ExportCSV(filename string) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() writer := csv.NewWriter(file) defer writer.Flush() // En-têtes CSV headers := []string{ "Timestamp", "Requests", "Success", "Errors", "ErrorRate(%)", "AvgLatency(ms)", "MinLatency(ms)", "MaxLatency(ms)", "RPS", } if err := writer.Write(headers); err != nil { return err } // Données avec validation for _, metric := range m.TimeSeries { // S'assurer que RPS n'est pas négatif ou invalide rps := metric.RPS if math.IsNaN(rps) || math.IsInf(rps, 0) || rps < 0 { rps = 0 } row := []string{ metric.Timestamp.Format("2006-01-02 15:04:05"), fmt.Sprintf("%d", metric.Requests), fmt.Sprintf("%d", metric.Success), fmt.Sprintf("%d", metric.Errors), fmt.Sprintf("%.2f", math.Max(0, metric.ErrorRate)), fmt.Sprintf("%.2f", math.Max(0, float64(metric.AvgLatency.Milliseconds()))), fmt.Sprintf("%.2f", math.Max(0, float64(metric.MinLatency.Milliseconds()))), fmt.Sprintf("%.2f", math.Max(0, float64(metric.MaxLatency.Milliseconds()))), fmt.Sprintf("%.2f", rps), } if err := writer.Write(row); err != nil { return err } } return nil } // Export JSON func (m *AggregatedMetrics) ExportJSON(filename string) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() encoder := json.NewEncoder(file) encoder.SetIndent("", " ") return encoder.Encode(m) } // Génération HTML avec graphique et validation func (m *AggregatedMetrics) GenerateHTMLReport(filename string) error { // Nettoyer les données pour HTML cleanedData := m.cleanTimeSeriesData() html := `
Generated: ` + time.Now().Format("2006-01-02 15:04:05") + `