Initial
This commit is contained in:
105
README.md
Normal file
105
README.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
# Avertissement légal
|
||||||
|
⚠️ N'utilisez ce code que sur des sites que vous possédez ou pour lesquels vous avez une autorisation explicite.
|
||||||
|
Le stress testing non autorisé peut être considéré comme une attaque par déni de service.
|
||||||
|
|
||||||
|
# Compilation
|
||||||
|
```
|
||||||
|
go build -o stress-test main.go
|
||||||
|
```
|
||||||
|
# Utilisation
|
||||||
|
## Aide
|
||||||
|
```
|
||||||
|
./stress-test -help
|
||||||
|
Usage of ./stress-test:
|
||||||
|
-csv string
|
||||||
|
Output CSV file (default "stress_test_results.csv")
|
||||||
|
-d int
|
||||||
|
Test duration in seconds
|
||||||
|
-html string
|
||||||
|
Output HTML report (default "stress_test_report.html")
|
||||||
|
-json string
|
||||||
|
Output JSON file (default "stress_test_results.json")
|
||||||
|
-r int
|
||||||
|
Total number of requests to make
|
||||||
|
-ru int
|
||||||
|
Ramp-up time in seconds
|
||||||
|
-t int
|
||||||
|
Number of threads (goroutines) (default 10)
|
||||||
|
-url string
|
||||||
|
Target URL to test
|
||||||
|
-ws int
|
||||||
|
Window size for time series in seconds (default 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test basique avec export
|
||||||
|
```
|
||||||
|
./stress-test -url https://mon.site.intranet -t 50 -d 60 -ru 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avec fenêtre de temps personnalisée (10 secondes)
|
||||||
|
```
|
||||||
|
./stress-test -url https://mon.site.intranet -t 100 -d 120 -ws 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avec tous les exports personnalisés
|
||||||
|
```
|
||||||
|
./stress-test -url https://mon.site.intranet -t 100 -d 300 -ru 30 \
|
||||||
|
-csv results.csv -json results.json -html report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basé sur le nombre de requêtes
|
||||||
|
```
|
||||||
|
./stress-test -url https://mon.site.intranet -t 50 -r 10000 -ws 5
|
||||||
|
```
|
||||||
|
|
||||||
|
# Exemple d'utilisation
|
||||||
|
```bash
|
||||||
|
./stress-test -url https://mon.site.intranet -t 100 -d 600 -ru 60
|
||||||
|
Starting stress test on https://mon.site.intranet
|
||||||
|
Threads: 100, Duration: 600s, Ramp-up: 60s
|
||||||
|
------------------------------------------------------------
|
||||||
|
```
|
||||||
|
Execution du test....
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
STRESS TEST RESULTS
|
||||||
|
============================================================
|
||||||
|
Total Duration: 606.10 seconds
|
||||||
|
Total Requests: 335949
|
||||||
|
Successful Requests: 335949
|
||||||
|
Failed Requests: 0
|
||||||
|
Error Rate: 0.00%
|
||||||
|
Requests per second: 554.28
|
||||||
|
|
||||||
|
Latency Metrics:
|
||||||
|
Average: 81.00 ms
|
||||||
|
Median: 76.00 ms
|
||||||
|
P95: 134.00 ms
|
||||||
|
P99: 166.00 ms
|
||||||
|
Min: 0.00 ms
|
||||||
|
Max: 403.00 ms
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
Exporting results...
|
||||||
|
✅ CSV exported to: stress_test_results.csv
|
||||||
|
✅ JSON exported to: stress_test_results.json
|
||||||
|
✅ HTML report generated: stress_test_report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## stress_test_report.html
|
||||||
|
Récupérer le fichier html et lire avec un navigateur
|
||||||
|
|
||||||
|
ℹ️ Le html utilise [Chart.js](https://cdn.jsdelivr.net/npm/chart.js), si besoin, récupérer le script localement et modifier le fichier html en remplacant la ligne:
|
||||||
|
```
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
```
|
||||||
|
Par
|
||||||
|
```
|
||||||
|
<script src="./chart.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Vue de l'état de l'application cible pendant le stress
|
||||||
|

|
||||||
BIN
imgs/stresstest-01.png
Normal file
BIN
imgs/stresstest-01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
imgs/stresstest-02.png
Normal file
BIN
imgs/stresstest-02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
857
main.go
Normal file
857
main.go
Normal file
@@ -0,0 +1,857 @@
|
|||||||
|
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 := `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Stress Test Report</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
||||||
|
.container { max-width: 1200px; margin: auto; background: white; padding: 20px; border-radius: 10px; }
|
||||||
|
h1, h2 { color: #333; }
|
||||||
|
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin: 20px 0; }
|
||||||
|
.metric-card { background: #f9f9f9; padding: 15px; border-radius: 5px; border-left: 4px solid #007bff; }
|
||||||
|
.metric-value { font-size: 24px; font-weight: bold; margin: 10px 0; }
|
||||||
|
.metric-label { color: #666; font-size: 12px; text-transform: uppercase; }
|
||||||
|
canvas { margin: 20px 0; max-height: 400px; }
|
||||||
|
.error-list { max-height: 300px; overflow-y: auto; }
|
||||||
|
.error-item { padding: 5px; border-bottom: 1px solid #eee; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Stress Test Report</h1>
|
||||||
|
<p>Generated: ` + time.Now().Format("2006-01-02 15:04:05") + `</p>
|
||||||
|
|
||||||
|
<h2>Summary Metrics</h2>
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Total Requests</div>
|
||||||
|
<div class="metric-value">` + fmt.Sprintf("%d", m.TotalRequests) + `</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Success Rate</div>
|
||||||
|
<div class="metric-value">` + fmt.Sprintf("%.2f", math.Max(0, 100-m.ErrorRate)) + `%</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Avg RPS</div>
|
||||||
|
<div class="metric-value">` + fmt.Sprintf("%.2f", math.Max(0, m.AvgRPS)) + `</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Avg Latency</div>
|
||||||
|
<div class="metric-value">` + fmt.Sprintf("%.2f", math.Max(0, float64(m.LatencyAvg.Milliseconds()))) + ` ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">P95 Latency</div>
|
||||||
|
<div class="metric-value">` + fmt.Sprintf("%.2f", math.Max(0, float64(m.LatencyP95.Milliseconds()))) + ` ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">P99 Latency</div>
|
||||||
|
<div class="metric-value">` + fmt.Sprintf("%.2f", math.Max(0, float64(m.LatencyP99.Milliseconds()))) + ` ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>RPS Over Time</h2>
|
||||||
|
<canvas id="rpsChart"></canvas>
|
||||||
|
|
||||||
|
<h2>Latency Over Time</h2>
|
||||||
|
<canvas id="latencyChart"></canvas>
|
||||||
|
|
||||||
|
<h2>Error Rate Over Time</h2>
|
||||||
|
<canvas id="errorChart"></canvas>
|
||||||
|
|
||||||
|
<h2>Errors by Type</h2>
|
||||||
|
<div class="error-list">
|
||||||
|
` + m.generateErrorList() + `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const timeLabels = ` + m.generateTimeLabels(cleanedData) + `;
|
||||||
|
|
||||||
|
// RPS Chart
|
||||||
|
new Chart(document.getElementById('rpsChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Requests per Second',
|
||||||
|
data: [` + m.generateFloatArray(cleanedData, func(t TimeSeriesMetric) float64 { return math.Max(0, t.RPS) }) + `],
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: { display: true, text: 'Requests per Second' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: 'Time' },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Latency Chart
|
||||||
|
new Chart(document.getElementById('latencyChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Avg Latency (ms)',
|
||||||
|
data: [` + m.generateFloatArray(cleanedData, func(t TimeSeriesMetric) float64 { return math.Max(0, float64(t.AvgLatency.Milliseconds())) }) + `],
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Min Latency (ms)',
|
||||||
|
data: [` + m.generateFloatArray(cleanedData, func(t TimeSeriesMetric) float64 { return math.Max(0, float64(t.MinLatency.Milliseconds())) }) + `],
|
||||||
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Max Latency (ms)',
|
||||||
|
data: [` + m.generateFloatArray(cleanedData, func(t TimeSeriesMetric) float64 { return math.Max(0, float64(t.MaxLatency.Milliseconds())) }) + `],
|
||||||
|
borderColor: 'rgb(255, 206, 86)',
|
||||||
|
backgroundColor: 'rgba(255, 206, 86, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: { display: true, text: 'Latency (ms)' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: 'Time' },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error Rate Chart
|
||||||
|
new Chart(document.getElementById('errorChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Error Rate (%)',
|
||||||
|
data: [` + m.generateFloatArray(cleanedData, func(t TimeSeriesMetric) float64 { return math.Max(0, math.Min(100, t.ErrorRate)) }) + `],
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
title: { display: true, text: 'Error Rate (%)' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: 'Time' },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
return os.WriteFile(filename, []byte(html), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoie les données aberrantes
|
||||||
|
func (m *AggregatedMetrics) cleanTimeSeriesData() []TimeSeriesMetric {
|
||||||
|
cleaned := make([]TimeSeriesMetric, 0, len(m.TimeSeries))
|
||||||
|
|
||||||
|
for _, metric := range m.TimeSeries {
|
||||||
|
// Filtrer les valeurs aberrantes
|
||||||
|
if metric.RPS < 0 || math.IsNaN(metric.RPS) || math.IsInf(metric.RPS, 0) {
|
||||||
|
metric.RPS = 0
|
||||||
|
}
|
||||||
|
if metric.ErrorRate < 0 || math.IsNaN(metric.ErrorRate) {
|
||||||
|
metric.ErrorRate = 0
|
||||||
|
}
|
||||||
|
if metric.ErrorRate > 100 {
|
||||||
|
metric.ErrorRate = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned = append(cleaned, metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AggregatedMetrics) generateTimeLabels(cleanedData []TimeSeriesMetric) string {
|
||||||
|
labels := make([]string, len(cleanedData))
|
||||||
|
for i, metric := range cleanedData {
|
||||||
|
labels[i] = `"` + metric.Timestamp.Format("15:04:05") + `"`
|
||||||
|
}
|
||||||
|
return "[" + strings.Join(labels, ",") + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction generateFloatArray corrigée
|
||||||
|
func (m *AggregatedMetrics) generateFloatArray(cleanedData []TimeSeriesMetric, extractor func(TimeSeriesMetric) float64) string {
|
||||||
|
values := make([]string, len(cleanedData))
|
||||||
|
for i, metric := range cleanedData {
|
||||||
|
val := extractor(metric)
|
||||||
|
if math.IsNaN(val) || math.IsInf(val, 0) {
|
||||||
|
val = 0
|
||||||
|
}
|
||||||
|
values[i] = fmt.Sprintf("%.2f", val)
|
||||||
|
}
|
||||||
|
return strings.Join(values, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction generateErrorList corrigée
|
||||||
|
func (m *AggregatedMetrics) generateErrorList() string {
|
||||||
|
if len(m.ErrorsByType) == 0 {
|
||||||
|
return "<div>Aucune erreur détectée</div>"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ""
|
||||||
|
for errType, count := range m.ErrorsByType {
|
||||||
|
result += fmt.Sprintf("<div class='error-item'><strong>%s</strong>: %d occurrences</div>", errType, count)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type Worker struct {
|
||||||
|
id int
|
||||||
|
url string
|
||||||
|
client *http.Client
|
||||||
|
metrics *Metrics
|
||||||
|
stopChan <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorker(id int, url string, metrics *Metrics, stopChan <-chan struct{}) *Worker {
|
||||||
|
transport := &http.Transport{
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Worker{
|
||||||
|
id: id,
|
||||||
|
url: url,
|
||||||
|
client: &http.Client{Transport: transport, Timeout: 30 * time.Second},
|
||||||
|
metrics: metrics,
|
||||||
|
stopChan: stopChan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) Run(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-w.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := w.client.Get(w.url)
|
||||||
|
latency := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.metrics.AddResult(latency, err, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
|
||||||
|
w.metrics.AddResult(latency, nil, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StressTest struct {
|
||||||
|
url string
|
||||||
|
totalThreads int
|
||||||
|
duration time.Duration
|
||||||
|
totalRequests int64
|
||||||
|
rampUp time.Duration
|
||||||
|
metrics *Metrics
|
||||||
|
workers []*Worker
|
||||||
|
stopChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStressTest(url string, threads int, duration time.Duration, requests int64, rampUp time.Duration, windowSize time.Duration) *StressTest {
|
||||||
|
return &StressTest{
|
||||||
|
url: url,
|
||||||
|
totalThreads: threads,
|
||||||
|
duration: duration,
|
||||||
|
totalRequests: requests,
|
||||||
|
rampUp: rampUp,
|
||||||
|
metrics: NewMetrics(windowSize),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *StressTest) Run() {
|
||||||
|
fmt.Printf("Starting stress test on %s\n", st.url)
|
||||||
|
fmt.Printf("Threads: %d, Duration: %.0fs, Ramp-up: %.0fs\n",
|
||||||
|
st.totalThreads, st.duration.Seconds(), st.rampUp.Seconds())
|
||||||
|
fmt.Println(stringRepeat("-", 60))
|
||||||
|
|
||||||
|
st.metrics.startTime = time.Now()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
rampStep := st.rampUp / time.Duration(st.totalThreads)
|
||||||
|
|
||||||
|
for i := 0; i < st.totalThreads; i++ {
|
||||||
|
worker := NewWorker(i, st.url, st.metrics, st.stopChan)
|
||||||
|
st.workers = append(st.workers, worker)
|
||||||
|
|
||||||
|
st.wg.Add(1)
|
||||||
|
go func(w *Worker, delay time.Duration) {
|
||||||
|
defer st.wg.Done()
|
||||||
|
if delay > 0 {
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
w.Run(ctx)
|
||||||
|
}(worker, time.Duration(i)*rampStep)
|
||||||
|
|
||||||
|
if st.rampUp > 0 {
|
||||||
|
time.Sleep(rampStep / 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.totalRequests > 0 {
|
||||||
|
go func() {
|
||||||
|
for atomic.LoadInt64(&st.metrics.totalReqs) < st.totalRequests {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
close(st.stopChan)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.duration > 0 {
|
||||||
|
time.Sleep(st.duration)
|
||||||
|
close(st.stopChan)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
st.wg.Wait()
|
||||||
|
st.metrics.SetEndTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringRepeat(s string, count int) string {
|
||||||
|
result := ""
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
result += s
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func printMetrics(metrics *AggregatedMetrics) {
|
||||||
|
fmt.Println("\n" + stringRepeat("=", 60))
|
||||||
|
fmt.Println("STRESS TEST RESULTS")
|
||||||
|
fmt.Println(stringRepeat("=", 60))
|
||||||
|
fmt.Printf("Total Duration: %.2f seconds\n", metrics.EndTime.Sub(metrics.StartTime).Seconds())
|
||||||
|
fmt.Printf("Total Requests: %d\n", metrics.TotalRequests)
|
||||||
|
fmt.Printf("Successful Requests: %d\n", metrics.SuccessRequests)
|
||||||
|
fmt.Printf("Failed Requests: %d\n", metrics.ErrorRequests)
|
||||||
|
fmt.Printf("Error Rate: %.2f%%\n", metrics.ErrorRate)
|
||||||
|
fmt.Printf("Requests per second: %.2f\n", metrics.AvgRPS)
|
||||||
|
|
||||||
|
if metrics.LatencyAvg > 0 {
|
||||||
|
fmt.Println("\nLatency Metrics:")
|
||||||
|
fmt.Printf(" Average: %.2f ms\n", float64(metrics.LatencyAvg.Milliseconds()))
|
||||||
|
fmt.Printf(" Median: %.2f ms\n", float64(metrics.LatencyMedian.Milliseconds()))
|
||||||
|
fmt.Printf(" P95: %.2f ms\n", float64(metrics.LatencyP95.Milliseconds()))
|
||||||
|
fmt.Printf(" P99: %.2f ms\n", float64(metrics.LatencyP99.Milliseconds()))
|
||||||
|
fmt.Printf(" Min: %.2f ms\n", float64(metrics.LatencyMin.Milliseconds()))
|
||||||
|
fmt.Printf(" Max: %.2f ms\n", float64(metrics.LatencyMax.Milliseconds()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(metrics.ErrorsByType) > 0 {
|
||||||
|
fmt.Println("\nError Breakdown:")
|
||||||
|
for errType, count := range metrics.ErrorsByType {
|
||||||
|
fmt.Printf(" %s: %d\n", errType, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
url string
|
||||||
|
threads int
|
||||||
|
duration int
|
||||||
|
requests int64
|
||||||
|
rampUp int
|
||||||
|
windowSize int
|
||||||
|
outputCSV string
|
||||||
|
outputJSON string
|
||||||
|
outputHTML string
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(&url, "url", "", "Target URL to test")
|
||||||
|
flag.IntVar(&threads, "t", 10, "Number of threads (goroutines)")
|
||||||
|
flag.IntVar(&duration, "d", 0, "Test duration in seconds")
|
||||||
|
flag.Int64Var(&requests, "r", 0, "Total number of requests to make")
|
||||||
|
flag.IntVar(&rampUp, "ru", 0, "Ramp-up time in seconds")
|
||||||
|
flag.IntVar(&windowSize, "ws", 5, "Window size for time series in seconds")
|
||||||
|
flag.StringVar(&outputCSV, "csv", "stress_test_results.csv", "Output CSV file")
|
||||||
|
flag.StringVar(&outputJSON, "json", "stress_test_results.json", "Output JSON file")
|
||||||
|
flag.StringVar(&outputHTML, "html", "stress_test_report.html", "Output HTML report")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if url == "" {
|
||||||
|
fmt.Println("Error: URL is required")
|
||||||
|
flag.Usage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration == 0 && requests == 0 {
|
||||||
|
fmt.Println("Error: You must specify either -d or -r")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
test := NewStressTest(
|
||||||
|
url,
|
||||||
|
threads,
|
||||||
|
time.Duration(duration)*time.Second,
|
||||||
|
requests,
|
||||||
|
time.Duration(rampUp)*time.Second,
|
||||||
|
time.Duration(windowSize)*time.Second,
|
||||||
|
)
|
||||||
|
test.Run()
|
||||||
|
|
||||||
|
metrics := test.metrics.GetAggregatedMetrics()
|
||||||
|
printMetrics(metrics)
|
||||||
|
|
||||||
|
// Export des résultats
|
||||||
|
fmt.Println("\n" + stringRepeat("-", 60))
|
||||||
|
fmt.Println("Exporting results...")
|
||||||
|
|
||||||
|
if err := metrics.ExportCSV(outputCSV); err != nil {
|
||||||
|
log.Printf("Error exporting CSV: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ CSV exported to: %s\n", outputCSV)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := metrics.ExportJSON(outputJSON); err != nil {
|
||||||
|
log.Printf("Error exporting JSON: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ JSON exported to: %s\n", outputJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := metrics.GenerateHTMLReport(outputHTML); err != nil {
|
||||||
|
log.Printf("Error generating HTML report: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ HTML report generated: %s\n", outputHTML)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
stress-test
Executable file
BIN
stress-test
Executable file
Binary file not shown.
208
stress_test_report.html
Normal file
208
stress_test_report.html
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Stress Test Report</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
||||||
|
.container { max-width: 1200px; margin: auto; background: white; padding: 20px; border-radius: 10px; }
|
||||||
|
h1, h2 { color: #333; }
|
||||||
|
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin: 20px 0; }
|
||||||
|
.metric-card { background: #f9f9f9; padding: 15px; border-radius: 5px; border-left: 4px solid #007bff; }
|
||||||
|
.metric-value { font-size: 24px; font-weight: bold; margin: 10px 0; }
|
||||||
|
.metric-label { color: #666; font-size: 12px; text-transform: uppercase; }
|
||||||
|
canvas { margin: 20px 0; max-height: 400px; }
|
||||||
|
.error-list { max-height: 300px; overflow-y: auto; }
|
||||||
|
.error-item { padding: 5px; border-bottom: 1px solid #eee; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Stress Test Report</h1>
|
||||||
|
<p>Generated: 2026-04-25 22:35:54</p>
|
||||||
|
|
||||||
|
<h2>Summary Metrics</h2>
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Total Requests</div>
|
||||||
|
<div class="metric-value">335949</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Success Rate</div>
|
||||||
|
<div class="metric-value">100.00%</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Avg RPS</div>
|
||||||
|
<div class="metric-value">554.28</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">Avg Latency</div>
|
||||||
|
<div class="metric-value">81.00 ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">P95 Latency</div>
|
||||||
|
<div class="metric-value">134.00 ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">P99 Latency</div>
|
||||||
|
<div class="metric-value">166.00 ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>RPS Over Time</h2>
|
||||||
|
<canvas id="rpsChart"></canvas>
|
||||||
|
|
||||||
|
<h2>Latency Over Time</h2>
|
||||||
|
<canvas id="latencyChart"></canvas>
|
||||||
|
|
||||||
|
<h2>Error Rate Over Time</h2>
|
||||||
|
<canvas id="errorChart"></canvas>
|
||||||
|
|
||||||
|
<h2>Errors by Type</h2>
|
||||||
|
<div class="error-list">
|
||||||
|
<div>Aucune erreur détectée</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const timeLabels = ["22:25:48","22:25:53","22:25:58","22:26:03","22:26:08","22:26:13","22:26:18","22:26:23","22:26:28","22:26:33","22:26:38","22:26:43","22:26:48","22:26:53","22:26:58","22:27:03","22:27:08","22:27:13","22:27:18","22:27:23","22:27:28","22:27:33","22:27:38","22:27:43","22:27:48","22:27:53","22:27:58","22:28:03","22:28:08","22:28:13","22:28:18","22:28:23","22:28:28","22:28:33","22:28:38","22:28:43","22:28:48","22:28:53","22:28:58","22:29:03","22:29:08","22:29:13","22:29:18","22:29:23","22:29:28","22:29:33","22:29:38","22:29:43","22:29:48","22:29:53","22:29:58","22:30:03","22:30:08","22:30:13","22:30:18","22:30:23","22:30:28","22:30:33","22:30:38","22:30:43","22:30:48","22:30:53","22:30:58","22:31:03","22:31:08","22:31:13","22:31:18","22:31:23","22:31:28","22:31:33","22:31:38","22:31:43","22:31:48","22:31:53","22:31:58","22:32:03","22:32:08","22:32:13","22:32:18","22:32:23","22:32:28","22:32:33","22:32:38","22:32:43","22:32:48","22:32:53","22:32:58","22:33:03","22:33:08","22:33:13","22:33:18","22:33:23","22:33:28","22:33:33","22:33:38","22:33:43","22:33:48","22:33:53","22:33:58","22:34:03","22:34:08","22:34:13","22:34:18","22:34:23","22:34:28","22:34:33","22:34:38","22:34:43","22:34:48","22:34:53","22:34:58","22:35:03","22:35:08","22:35:13","22:35:18","22:35:23","22:35:28","22:35:33","22:35:38","22:35:43","22:35:48","22:35:53"];
|
||||||
|
|
||||||
|
// RPS Chart
|
||||||
|
new Chart(document.getElementById('rpsChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Requests per Second',
|
||||||
|
data: [619.69,648.42,641.16,682.09,744.66,567.38,454.94,468.47,454.74,434.35,406.74,446.45,624.67,590.69,614.57,728.16,654.03,682.09,479.71,474.66,461.95,477.86,469.07,454.97,557.20,601.21,648.40,636.91,705.74,700.97,502.31,470.95,472.41,473.38,475.95,459.12,470.79,669.21,607.68,630.38,707.98,659.33,599.97,474.19,473.49,483.20,472.80,445.80,470.55,582.10,565.29,638.73,642.62,714.38,641.69,475.18,463.17,480.45,470.18,453.12,466.31,564.10,625.59,597.41,681.39,684.50,683.79,511.00,459.09,469.12,472.77,455.77,465.22,532.59,611.48,644.78,655.79,646.57,722.74,568.14,451.43,476.56,478.74,456.96,469.98,482.45,622.32,642.95,606.93,680.04,665.61,651.16,464.14,480.32,475.97,446.70,466.91,456.04,611.98,596.76,645.54,658.86,666.10,704.09,475.52,481.27,471.74,448.53,465.66,403.86,498.46,614.74,648.77,632.50,695.50,649.66,573.81,470.67,470.76,453.76,468.74,507.20],
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: { display: true, text: 'Requests per Second' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: 'Time' },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Latency Chart
|
||||||
|
new Chart(document.getElementById('latencyChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Avg Latency (ms)',
|
||||||
|
data: [3.00,9.00,15.00,19.00,23.00,35.00,52.00,58.00,67.00,79.00,92.00,92.00,74.00,81.00,79.00,67.00,74.00,71.00,97.00,99.00,102.00,98.00,100.00,103.00,86.00,80.00,75.00,76.00,70.00,70.00,94.00,100.00,99.00,99.00,98.00,102.00,100.00,73.00,79.00,77.00,69.00,74.00,80.00,100.00,99.00,97.00,99.00,106.00,100.00,84.00,86.00,76.00,76.00,69.00,75.00,100.00,101.00,97.00,99.00,104.00,100.00,85.00,77.00,81.00,72.00,71.00,71.00,92.00,102.00,100.00,99.00,104.00,100.00,90.00,79.00,75.00,74.00,75.00,68.00,84.00,104.00,98.00,98.00,102.00,100.00,97.00,78.00,76.00,79.00,72.00,73.00,75.00,101.00,98.00,98.00,105.00,100.00,103.00,79.00,80.00,75.00,74.00,73.00,69.00,98.00,98.00,99.00,104.00,101.00,114.00,97.00,79.00,74.00,77.00,70.00,75.00,83.00,99.00,100.00,103.00,101.00,96.00],
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Min Latency (ms)',
|
||||||
|
data: [0.00,1.00,3.00,6.00,11.00,4.00,11.00,14.00,15.00,15.00,34.00,15.00,41.00,31.00,28.00,39.00,33.00,43.00,24.00,39.00,20.00,34.00,33.00,33.00,30.00,28.00,33.00,33.00,33.00,26.00,29.00,39.00,38.00,36.00,35.00,15.00,24.00,30.00,40.00,31.00,49.00,26.00,26.00,29.00,44.00,42.00,35.00,19.00,37.00,26.00,34.00,28.00,28.00,31.00,34.00,24.00,28.00,42.00,38.00,36.00,39.00,32.00,29.00,19.00,20.00,32.00,30.00,34.00,37.00,36.00,41.00,33.00,35.00,37.00,30.00,27.00,35.00,21.00,48.00,22.00,25.00,25.00,34.00,38.00,14.00,28.00,32.00,41.00,27.00,24.00,26.00,28.00,22.00,33.00,34.00,19.00,36.00,25.00,13.00,28.00,19.00,31.00,26.00,18.00,44.00,28.00,40.00,27.00,35.00,32.00,39.00,33.00,22.00,34.00,33.00,18.00,33.00,34.00,38.00,31.00,31.00,51.00],
|
||||||
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Max Latency (ms)',
|
||||||
|
data: [22.00,30.00,44.00,88.00,67.00,125.00,137.00,169.00,172.00,206.00,224.00,207.00,183.00,217.00,258.00,210.00,192.00,197.00,223.00,200.00,239.00,264.00,250.00,279.00,232.00,208.00,220.00,188.00,199.00,180.00,237.00,223.00,234.00,191.00,208.00,261.00,224.00,188.00,231.00,266.00,164.00,197.00,206.00,215.00,212.00,187.00,263.00,246.00,211.00,302.00,271.00,268.00,194.00,158.00,190.00,213.00,237.00,201.00,186.00,287.00,223.00,250.00,272.00,211.00,238.00,247.00,216.00,208.00,259.00,208.00,284.00,232.00,217.00,196.00,249.00,273.00,188.00,190.00,177.00,193.00,233.00,211.00,230.00,254.00,275.00,199.00,199.00,214.00,218.00,246.00,168.00,192.00,249.00,208.00,227.00,247.00,187.00,228.00,242.00,197.00,251.00,248.00,208.00,154.00,218.00,191.00,199.00,234.00,225.00,381.00,403.00,179.00,216.00,236.00,209.00,229.00,196.00,200.00,217.00,221.00,211.00,150.00],
|
||||||
|
borderColor: 'rgb(255, 206, 86)',
|
||||||
|
backgroundColor: 'rgba(255, 206, 86, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: { display: true, text: 'Latency (ms)' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: 'Time' },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error Rate Chart
|
||||||
|
new Chart(document.getElementById('errorChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Error Rate (%)',
|
||||||
|
data: [0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00],
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
title: { display: true, text: 'Error Rate (%)' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: 'Time' },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user