Files
omnex/control-plane/api/cmd/loadtest/main.go
T
Xor290 2f6e5a953d
ci-api / test (push) Successful in 28m2s
chore: update
2026-08-08 18:27:52 +02:00

154 lines
4.1 KiB
Go

// cmd/loadtest : script de charge maison pour l'API control-plane (aucun
// outil externe type k6/locust — un seul binaire Go qui mesure latence et
// taux d'erreur sous charge concurrente sur un endpoint authentifié).
//
// Usage :
//
// go run ./cmd/loadtest --url https://control-plane.example \
// --username admin --password *** \
// --endpoint /api/v1/demos --concurrency 20 --duration 30s
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"sort"
"sync"
"sync/atomic"
"time"
)
func main() {
baseURL := flag.String("url", "http://localhost:8080", "URL de base de l'API")
username := flag.String("username", "", "identifiant admin (pour /auth/login)")
password := flag.String("password", "", "mot de passe admin")
endpoint := flag.String("endpoint", "/api/v1/demos", "endpoint GET à charger (authentifié)")
concurrency := flag.Int("concurrency", 10, "nombre de workers concurrents")
duration := flag.Duration("duration", 30*time.Second, "durée du test")
flag.Parse()
if *username == "" || *password == "" {
log.Fatal("--username et --password requis (compte admin)")
}
token, err := login(*baseURL, *username, *password)
if err != nil {
log.Fatalf("login: %v", err)
}
log.Printf("connecté — %d workers pendant %s sur %s%s", *concurrency, *duration, *baseURL, *endpoint)
var (
mu sync.Mutex
latencies []time.Duration
okCount int64
errCount int64
)
ctx, cancel := context.WithTimeout(context.Background(), *duration)
defer cancel()
client := &http.Client{Timeout: 10 * time.Second}
var wg sync.WaitGroup
for i := 0; i < *concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
}
start := time.Now()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, *baseURL+*endpoint, nil)
if err != nil {
atomic.AddInt64(&errCount, 1)
continue
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
elapsed := time.Since(start)
if err != nil {
atomic.AddInt64(&errCount, 1)
continue
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode >= 400 {
atomic.AddInt64(&errCount, 1)
continue
}
atomic.AddInt64(&okCount, 1)
mu.Lock()
latencies = append(latencies, elapsed)
mu.Unlock()
}
}()
}
wg.Wait()
report(latencies, okCount, errCount, *duration)
}
func login(baseURL, username, password string) (string, error) {
body, err := json.Marshal(map[string]string{"username": username, "password": password})
if err != nil {
return "", err
}
resp, err := http.Post(baseURL+"/api/v1/auth/login", "application/json", bytes.NewReader(body))
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("statut %d: %s", resp.StatusCode, b)
}
var out struct {
Token string `json:"token"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", err
}
return out.Token, nil
}
func report(latencies []time.Duration, ok, errs int64, duration time.Duration) {
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
percentile := func(p float64) time.Duration {
if len(latencies) == 0 {
return 0
}
idx := int(float64(len(latencies)-1) * p)
return latencies[idx]
}
total := ok + errs
var errRate float64
if total > 0 {
errRate = 100 * float64(errs) / float64(total)
}
fmt.Println()
fmt.Println("=== Résultat ===")
fmt.Printf("Durée : %s\n", duration)
fmt.Printf("Requêtes : %d (%.1f req/s)\n", total, float64(total)/duration.Seconds())
fmt.Printf("Succès / Erreurs : %d / %d (%.2f%% d'erreurs)\n", ok, errs, errRate)
if len(latencies) > 0 {
fmt.Printf("Latence p50/p95/p99 : %s / %s / %s\n", percentile(0.50), percentile(0.95), percentile(0.99))
fmt.Printf("Latence min/max : %s / %s\n", latencies[0], latencies[len(latencies)-1])
} else {
fmt.Println("Aucune requête réussie — pas de statistiques de latence.")
}
}