This commit is contained in:
stef
2026-04-25 23:14:07 +02:00
commit 9fef8ec260
7 changed files with 1184 additions and 0 deletions

105
README.md Normal file
View 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>
```
![stress_test_report.html](./imgs/stresstest-01.png)
## Vue de l'état de l'application cible pendant le stress
![Vue perf applis](./imgs/stresstest-02.png)

14
chart.js Normal file

File diff suppressed because one or more lines are too long

BIN
imgs/stresstest-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
imgs/stresstest-02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

857
main.go Normal file
View 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

Binary file not shown.

208
stress_test_report.html Normal file
View 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>