From 2ed9e1cee4ae01f4cbbf7ea90ec59f20a4594495 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Wed, 23 Sep 2026 09:29:20 +0200 Subject: [PATCH] test(charge): mesure l'API sous charge avec k6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aucun garde-fou de performance n'existait, et le dépôt ne chiffrait aucun temps de réponse. tests/load, quatre scénarios : - smoke : une minute sur chaque route de lecture, joué à chaque PR ; - charge : 50 utilisateurs, 40 sur le tableau de bord au rythme de son rafraîchissement, 10 qui explorent les sites ; - stress : débit croissant jusqu'à la rupture, arrêt au-delà de 10 % d'erreurs ; - limitation-debit : par le proxy, vérifie que nginx répond 429 et jamais 5xx. Seuils : p95 < 500 ms et p99 < 1 s sur les lectures, moins de 1 % d'échecs. k6 tourne en service Compose (profil load) sur le réseau du projet : il vise backend:8000 et mesure l'API plutôt que la limite de 20 req/s par adresse de nginx. Chaque tir écrit un rapport HTML, une synthèse Markdown et le JSON brut dans tests/load/results. make load-smoke, load-test, load-stress et load-limits ; le job E2E enchaîne le smoke et le test de limitation après Playwright. Closes #47 --- .github/workflows/e2e.yml | 31 +++++++++++- Makefile | 21 +++++++- docker-compose.yml | 17 +++++++ tests/load/README.md | 92 ++++++++++++++++++++++++++++++++++ tests/load/charge.js | 56 +++++++++++++++++++++ tests/load/lib/api.js | 63 +++++++++++++++++++++++ tests/load/lib/config.js | 19 +++++++ tests/load/lib/rapport.js | 47 +++++++++++++++++ tests/load/limitation-debit.js | 43 ++++++++++++++++ tests/load/smoke.js | 29 +++++++++++ tests/load/stress.js | 44 ++++++++++++++++ 11 files changed, 460 insertions(+), 2 deletions(-) create mode 100644 tests/load/README.md create mode 100644 tests/load/charge.js create mode 100644 tests/load/lib/api.js create mode 100644 tests/load/lib/config.js create mode 100644 tests/load/lib/rapport.js create mode 100644 tests/load/limitation-debit.js create mode 100644 tests/load/smoke.js create mode 100644 tests/load/stress.js diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 805703d..d3c6268 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -15,7 +15,7 @@ permissions: jobs: parcours: - name: Parcours Playwright + name: Parcours Playwright et tirs k6 runs-on: ubuntu-latest timeout-minutes: 30 env: @@ -82,6 +82,35 @@ jobs: E2E_COMPTES: ${{ runner.temp }}/comptes.json run: npx playwright test + # Direct sur `backend:8000` : ce tir mesure l'API, pas la limitation de nginx. + - name: Tir k6 de fumée sur l'API + env: + K6_RESUME: /results/resume-smoke.md + run: | + K6_EMAIL="$(jq -r .lecteur.email "$RUNNER_TEMP/comptes.json")" + K6_PASSWORD="$(jq -r .lecteur.password "$RUNNER_TEMP/comptes.json")" + echo "::add-mask::$K6_PASSWORD" + export K6_EMAIL K6_PASSWORD + make load-smoke + + - name: Vérifie par k6 que le proxy limite le débit + env: + K6_RESUME: /results/resume-limitation.md + run: make load-limits + + - name: Publie la synthèse k6 + if: ${{ !cancelled() }} + run: cat tests/load/results/resume-*.md >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true + + - name: Publie les rapports k6 + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: k6-rapports + path: tests/load/results/ + if-no-files-found: ignore + retention-days: 14 + - name: Publie le rapport Playwright if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 diff --git a/Makefile b/Makefile index 64615cc..b410333 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,13 @@ ML_TEST_DATABASE_URL ?= postgresql+psycopg://$(PG_USER):$(PG_PASSWORD)@localhost E2E_COMPTES ?= $(CURDIR)/$(E2E)/.comptes.json E2E_API ?= http://localhost:$(or $(strip $(call env-val,BACKEND_PORT)),8000) +# Piege : `run` ne demarre que k6, la stack doit deja tourner. `--user` fait ecrire les rapports +# de tests/load/results avec l'uid du poste, pas celui de l'image (12345), qui n'y a pas acces. +k6-run = mkdir -p tests/load/results && $(COMPOSE_PROD) --profile load run --rm \ + --user "$$(id -u):$$(id -g)" -e K6_WEB_DASHBOARD=true \ + -e K6_WEB_DASHBOARD_EXPORT=/results/$(1)-$$(date +%Y%m%dT%H%M%S).html \ + k6 run /scripts/$(1).js + # Le jeu historique s'arrete au 31/12/2024 : score et detection ancres a l'horloge reelle ne # verraient qu'un parc muet depuis des mois. Cf. `--now` de enervision_ml.score. DEMO_NOW ?= 2024-12-31T00:00:00Z @@ -54,7 +61,7 @@ DEMO_NOW ?= 2024-12-31T00:00:00Z ml-lint ml-typecheck ml-test ml-check ml-train ml-score mlflow-up detect-alerts recommendations \ airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \ tls-selfsigned tls-acme tls-renew stack-up stack-down stack-logs \ - e2e-install e2e-prepare e2e + e2e-install e2e-prepare e2e load-smoke load-test load-stress load-limits help: ## Liste les cibles disponibles @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' @@ -223,6 +230,18 @@ e2e-prepare: ## Sème le jeu de démonstration et crée les comptes de test sur e2e: ## Joue les parcours Playwright. E2E_BASE_URL= optionnel (défaut http://localhost:4200) cd $(E2E) && E2E_COMPTES=$(E2E_COMPTES) npx playwright test +load-smoke: ## Tir k6 d'une minute. K6_EMAIL= et K6_PASSWORD= d'un lecteur, K6_BASE_URL= optionnel + $(call k6-run,smoke) + +load-test: ## Charge nominale k6, 50 utilisateurs pendant 8 minutes. Rapport HTML dans tests/load/results + $(call k6-run,charge) + +load-stress: ## Monte le débit jusqu'à la rupture de l'API. Sur la VM, la prod partage la machine + $(call k6-run,stress) + +load-limits: ## Vérifie par le proxy que nginx limite le débit d'une même adresse (429) + $(call k6-run,limitation-debit) + db-up: ## Démarre la base PostgreSQL TimescaleDB docker compose up -d db diff --git a/docker-compose.yml b/docker-compose.yml index b6d9f5d..9eab014 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -193,6 +193,23 @@ services: airflow-init: condition: service_completed_successfully + # Pourquoi : sur le réseau du projet, k6 joint `backend:8000` sans passer par nginx, dont la + # limite par adresse (20 req/s) fausserait la mesure de l'API. `make load-*` le lance (ADR 0015). + k6: + image: grafana/k6:2.3.0 + profiles: ["load"] + volumes: + - ./tests/load:/scripts:ro + - ./tests/load/results:/results + environment: + K6_BASE_URL: ${K6_BASE_URL:-http://backend:8000} + K6_PROXY_URL: ${K6_PROXY_URL:-https://proxy} + K6_EMAIL: ${K6_EMAIL:-} + K6_PASSWORD: ${K6_PASSWORD:-} + K6_RESUME: ${K6_RESUME:-} + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: pgdata: airflow_logs: diff --git a/tests/load/README.md b/tests/load/README.md new file mode 100644 index 0000000..97acb94 --- /dev/null +++ b/tests/load/README.md @@ -0,0 +1,92 @@ +# Tests de charge (k6) + +Scénarios k6 contre l'API EnerVision. Issue #47, décisions dans l'ADR 0015. + +## Scénarios + +| Script | Cible | Profil | Quand | +|---|---|---|---| +| `smoke.js` | API directe | 2 utilisateurs pendant 1 min, chaque route de lecture | Chaque PR (job E2E), `make load-smoke` | +| `charge.js` | API directe | 50 utilisateurs simultanés : montée 2 min, plateau 5 min, descente 1 min | `make load-test`, avant une livraison | +| `stress.js` | API directe | Débit croissant de 5 à 200 req/s, arrêt au-delà de 10 % d'erreurs | `make load-stress`, pour situer la rupture | +| `limitation-debit.js` | Proxy nginx | 80 req/s pendant 15 s depuis une seule adresse | Chaque PR (job E2E), `make load-limits` | + +### Hypothèses de charge + +Le cahier des charges ne chiffre ni volume d'utilisateurs ni temps de réponse. Les hypothèses +retenues : + +- **50 utilisateurs simultanés.** C'est un gestionnaire d'énergie par site suivi, plus + l'exploitation, avec de la marge. +- **40 gardent le tableau de bord ouvert.** Le frontend interroge `/stats/summary` toutes les + 10 s et `/alerts` toutes les 60 s, et charge `/predictions` une fois (`dashboard.ts`, + `alert-feed.ts`). +- **10 explorent les sites.** Ils ouvrent la liste, puis un détail avec sa mesure courante, ses + relevés sur 24 h et ses recommandations, avec 3 à 8 s de lecture entre deux pages. + +Soit environ 12 req/s en régime établi, avec des pointes au démarrage des sessions. + +### Seuils + +Les seuils sont posés par l'ADR 0015 et partagés par `lib/config.js`. + +| Seuil | Valeur | Raison | +|---|---|---| +| `http_req_failed` | < 1 % | Aucune erreur attendue en charge nominale | +| `checks` | > 99 % | Chaque réponse est un 200 | +| Lectures, p95 | < 500 ms | Rafraîchissement du tableau de bord imperceptible | +| Lectures, p99 | < 1 s | Borne des cas les plus lents | +| `/stats/summary`, p95 | < 500 ms | Route la plus appelée (`DISTINCT ON` sur tout l'historique) | +| `/readings`, p95 | < 800 ms | Fenêtre de 24 h, la plus volumineuse | + +Un seuil franchi fait échouer le tir (code de sortie 99). Le test de stress n'a qu'un seuil +bloquant, 10 % d'erreurs : il cherche la rupture, pas un niveau de service. + +## Pourquoi k6 ne passe pas par le proxy + +nginx limite chaque adresse IP à 20 req/s, avec une rafale de 40 (ADR 0007). Un tir depuis une +seule machine mesurerait cette limite, pas l'API. + +k6 tourne donc en service Compose (profil `load`) sur le réseau du projet, et vise +`backend:8000`. Seul `limitation-debit.js` passe par `https://proxy`, précisément pour vérifier +que la limite tient : des 429 au-delà du débit autorisé, jamais d'erreur serveur. + +## Lancer un tir + +Il faut une stack démarrée, des données et un compte `lecteur` **déjà activé** (mot de passe +définitif). + +```bash +# Poste, stack de `make dev` (API sur l'hôte) : jeu de démonstration et comptes de test +make e2e-prepare +make load-smoke K6_BASE_URL=http://host.docker.internal:8000 \ + K6_EMAIL="$(jq -r .lecteur.email tests/e2e/.comptes.json)" \ + K6_PASSWORD="$(jq -r .lecteur.password tests/e2e/.comptes.json)" + +# Poste, stack conteneurisée (make stack-up) : l'API est joignable en backend:8000 +make load-test K6_EMAIL=... K6_PASSWORD=... + +# Recette, sur la VM, dans /srv/enervision/rec : compte lecteur créé depuis l'interface +make load-test K6_EMAIL=charge@enervision.fr K6_PASSWORD=... +``` + +**Recette et prod partagent la VM** (ADR 0009). `make load-test` y reste raisonnable. +`make load-stress` sature l'hôte et ralentit la prod : ne le lancer qu'en accord avec l'équipe, +hors démonstration. + +`scripts/comptes-test.sh` ne sert qu'aux bases jetables. En recette, un administrateur crée le +compte `lecteur` du tir depuis l'interface, puis on s'y connecte une fois pour changer le mot de +passe temporaire. + +## Lire les résultats + +Chaque tir écrit dans `tests/load/results/` (ignoré par git) : + +- `-.html` : rapport du tableau de bord web de k6 (courbes de débit, de + latence et de VUs), à joindre au dossier de preuves ; +- `-.md` : synthèse et état de chaque seuil, aussi affichée en console et, + en CI, dans le résumé du job ; +- `-.json` : métriques brutes. + +Pendant un tir, le tableau de bord Grafana « API » (profil `monitoring`) montre le débit et les +latences vus par l'API elle-même. diff --git a/tests/load/charge.js b/tests/load/charge.js new file mode 100644 index 0000000..6cf0d3f --- /dev/null +++ b/tests/load/charge.js @@ -0,0 +1,56 @@ +// Charge nominale : 50 utilisateurs simultanés. 40 gardent le tableau de bord ouvert, qui +// interroge l'API au rythme du frontend ; 10 explorent les sites. +import { sleep } from 'k6'; + +import { auHasard, consulterSite, lire, preparer } from './lib/api.js'; +import { SEUILS, STATISTIQUES } from './lib/config.js'; +import { rapport } from './lib/rapport.js'; + +const paliers = (cible) => [ + { duration: '2m', target: cible }, + { duration: '5m', target: cible }, + { duration: '1m', target: 0 }, +]; + +export const options = { + insecureSkipTLSVerify: true, + scenarios: { + tableau_de_bord: { + executor: 'ramping-vus', + stages: paliers(40), + exec: 'tableauDeBord', + gracefulRampDown: '30s', + }, + exploration: { + executor: 'ramping-vus', + stages: paliers(10), + exec: 'exploration', + gracefulRampDown: '30s', + }, + }, + thresholds: SEUILS, + summaryTrendStats: STATISTIQUES, +}; + +export const setup = preparer; + +// Une itération vaut une minute d'écran ouvert : `/stats/summary` toutes les 10 s, les alertes +// toutes les 60 s, les prévisions une fois (dashboard.ts, alert-feed.ts). +export function tableauDeBord(data) { + lire('/predictions', 'predictions', data.jeton); + lire('/alerts', 'alerts', data.jeton); + for (let i = 0; i < 6; i += 1) { + lire('/stats/summary', 'stats_summary', data.jeton); + sleep(10); + } +} + +export function exploration(data) { + lire('/sites', 'sites', data.jeton); + consulterSite(auHasard(data.sites), data.jeton); + sleep(3 + Math.random() * 5); +} + +export function handleSummary(data) { + return rapport('charge', data); +} diff --git a/tests/load/lib/api.js b/tests/load/lib/api.js new file mode 100644 index 0000000..177554d --- /dev/null +++ b/tests/load/lib/api.js @@ -0,0 +1,63 @@ +import { check, fail } from 'k6'; +import http from 'k6/http'; + +import { API, COMPTE } from './config.js'; + +export function connecter() { + if (!COMPTE.email || !COMPTE.password) { + fail('K6_EMAIL et K6_PASSWORD désignent un compte lecteur déjà activé'); + } + const reponse = http.post(`${API}/auth/login`, JSON.stringify(COMPTE), { + headers: { 'Content-Type': 'application/json' }, + tags: { name: 'auth_login', type: 'auth' }, + }); + if (!check(reponse, { 'connexion acceptée': (r) => r.status === 200 })) { + fail(`connexion refusée : ${reponse.status} ${reponse.body}`); + } + return reponse.json('access_token'); +} + +// Jeton propre à chaque VU, repris sur un 401 : un tir plus long que le TTL du jeton (15 min) +// ne doit pas finir en erreurs. Jamais de refresh, dont la rotation révoquerait la session. +let jetonDuVu = null; + +export function lire(chemin, nom, jetonInitial) { + const envoyer = () => + http.get(`${API}${chemin}`, { + headers: { Authorization: `Bearer ${jetonDuVu || jetonInitial}` }, + tags: { name: nom, type: 'lecture' }, + }); + let reponse = envoyer(); + if (reponse.status === 401) { + jetonDuVu = connecter(); + reponse = envoyer(); + } + check(reponse, { [`${nom} répond 200`]: (r) => r.status === 200 }); + return reponse; +} + +export function preparer() { + const jeton = connecter(); + const sites = lire('/sites', 'sites', jeton).json(); + if (!Array.isArray(sites) || sites.length === 0) { + fail('aucun site en base : semer db/seeds/demo.sql ou importer des relevés avant le tir'); + } + return { jeton, sites: sites.map((site) => site.site_id) }; +} + +export function auHasard(liste) { + return liste[Math.floor(Math.random() * liste.length)]; +} + +// Même fenêtre que le détail d'un site dans le frontend : les 24 heures qui précèdent sa +// dernière mesure, pas celles qui précèdent l'instant présent. +export function consulterSite(siteId, jeton) { + lire(`/sites/${siteId}`, 'site', jeton); + const courant = lire(`/sites/${siteId}/current`, 'site_current', jeton); + const horodatage = courant.status === 200 ? courant.json('timestamp') : null; + const fin = horodatage ? new Date(horodatage) : new Date(); + const debut = new Date(fin.getTime() - 24 * 3600 * 1000); + const fenetre = `start=${encodeURIComponent(debut.toISOString())}&end=${encodeURIComponent(fin.toISOString())}`; + lire(`/readings?site_id=${siteId}&${fenetre}`, 'readings', jeton); + lire(`/recommendations?site_id=${siteId}`, 'recommendations', jeton); +} diff --git a/tests/load/lib/config.js b/tests/load/lib/config.js new file mode 100644 index 0000000..cc9386e --- /dev/null +++ b/tests/load/lib/config.js @@ -0,0 +1,19 @@ +export const BASE_URL = __ENV.K6_BASE_URL || 'http://backend:8000'; +export const API = `${BASE_URL}/api/v1`; +export const PROXY_URL = __ENV.K6_PROXY_URL || 'https://proxy'; + +export const COMPTE = { + email: __ENV.K6_EMAIL, + password: __ENV.K6_PASSWORD, +}; + +// Seuils posés par l'ADR 0015, faute d'exigence chiffrée dans le cahier des charges. +export const SEUILS = { + http_req_failed: ['rate<0.01'], + checks: ['rate>0.99'], + 'http_req_duration{type:lecture}': ['p(95)<500', 'p(99)<1000'], + 'http_req_duration{name:stats_summary}': ['p(95)<500'], + 'http_req_duration{name:readings}': ['p(95)<800'], +}; + +export const STATISTIQUES = ['avg', 'min', 'med', 'p(90)', 'p(95)', 'p(99)', 'max']; diff --git a/tests/load/lib/rapport.js b/tests/load/lib/rapport.js new file mode 100644 index 0000000..5537e30 --- /dev/null +++ b/tests/load/lib/rapport.js @@ -0,0 +1,47 @@ +export function rapport(nom, data) { + const horodatage = new Date().toISOString().replace(/[:.]/g, '-'); + const markdown = enMarkdown(nom, data); + const sorties = { + stdout: `${markdown}\n`, + [`/results/${nom}-${horodatage}.json`]: JSON.stringify(data, null, 2), + [`/results/${nom}-${horodatage}.md`]: markdown, + }; + if (__ENV.K6_RESUME) { + sorties[__ENV.K6_RESUME] = markdown; + } + return sorties; +} + +function valeur(metrique, statistique, unite) { + const brut = metrique?.values?.[statistique]; + if (brut === undefined) { + return '-'; + } + return unite === '%' ? `${(brut * 100).toFixed(2)} %` : `${brut.toFixed(1)} ${unite}`; +} + +function enMarkdown(nom, data) { + const m = data.metrics; + const lignes = [ + `### Tir k6 « ${nom} »`, + '', + '| Mesure | Valeur |', + '|---|---|', + `| Requêtes | ${m.http_reqs?.values?.count ?? 0} (${valeur(m.http_reqs, 'rate', 'req/s')}) |`, + `| Échecs HTTP | ${valeur(m.http_req_failed, 'rate', '%')} |`, + `| Vérifications réussies | ${valeur(m.checks, 'rate', '%')} |`, + `| Durée médiane | ${valeur(m.http_req_duration, 'med', 'ms')} |`, + `| Durée p95 | ${valeur(m.http_req_duration, 'p(95)', 'ms')} |`, + `| Durée p99 | ${valeur(m.http_req_duration, 'p(99)', 'ms')} |`, + `| VUs au plus haut | ${m.vus_max?.values?.max ?? '-'} |`, + '', + '| Seuil | Résultat |', + '|---|---|', + ]; + for (const [metrique, detail] of Object.entries(m)) { + for (const [expression, resultat] of Object.entries(detail.thresholds || {})) { + lignes.push(`| \`${metrique}\` ${expression} | ${resultat.ok ? 'tenu' : '**franchi**'} |`); + } + } + return lignes.join('\n'); +} diff --git a/tests/load/limitation-debit.js b/tests/load/limitation-debit.js new file mode 100644 index 0000000..0e080a4 --- /dev/null +++ b/tests/load/limitation-debit.js @@ -0,0 +1,43 @@ +// Vérifie par le proxy que la limitation de nginx tient : au-delà de 20 requêtes par seconde et +// de la rafale de 40, une même adresse doit recevoir des 429, et jamais une erreur serveur. +import { check } from 'k6'; +import http from 'k6/http'; +import { Counter } from 'k6/metrics'; + +import { PROXY_URL, STATISTIQUES } from './lib/config.js'; +import { rapport } from './lib/rapport.js'; + +const limitees = new Counter('reponses_limitees'); + +http.setResponseCallback(http.expectedStatuses(200, 429)); + +export const options = { + insecureSkipTLSVerify: true, + scenarios: { + rafale: { + executor: 'constant-arrival-rate', + rate: 80, + timeUnit: '1s', + duration: '15s', + preAllocatedVUs: 20, + maxVUs: 80, + }, + }, + thresholds: { + reponses_limitees: ['count>0'], + http_req_failed: ['rate<0.01'], + }, + summaryTrendStats: STATISTIQUES, +}; + +export default function () { + const reponse = http.get(`${PROXY_URL}/api/v1/health/live`, { tags: { name: 'health_live' } }); + if (reponse.status === 429) { + limitees.add(1); + } + check(reponse, { 'servie ou limitée, jamais en erreur': (r) => [200, 429].includes(r.status) }); +} + +export function handleSummary(data) { + return rapport('limitation-debit', data); +} diff --git a/tests/load/smoke.js b/tests/load/smoke.js new file mode 100644 index 0000000..f74d632 --- /dev/null +++ b/tests/load/smoke.js @@ -0,0 +1,29 @@ +import { sleep } from 'k6'; + +import { auHasard, consulterSite, lire, preparer } from './lib/api.js'; +import { SEUILS, STATISTIQUES } from './lib/config.js'; +import { rapport } from './lib/rapport.js'; + +export const options = { + insecureSkipTLSVerify: true, + scenarios: { + smoke: { executor: 'constant-vus', vus: 2, duration: '1m' }, + }, + thresholds: SEUILS, + summaryTrendStats: STATISTIQUES, +}; + +export const setup = preparer; + +export default function (data) { + lire('/stats/summary', 'stats_summary', data.jeton); + lire('/predictions', 'predictions', data.jeton); + lire('/alerts', 'alerts', data.jeton); + lire('/sites', 'sites', data.jeton); + consulterSite(auHasard(data.sites), data.jeton); + sleep(1); +} + +export function handleSummary(data) { + return rapport('smoke', data); +} diff --git a/tests/load/stress.js b/tests/load/stress.js new file mode 100644 index 0000000..814f990 --- /dev/null +++ b/tests/load/stress.js @@ -0,0 +1,44 @@ +// Monte le débit jusqu'à la rupture pour situer la limite de l'API. S'arrête de lui-même au-delà +// de 10 % d'erreurs : le but est de trouver le point de rupture, pas de s'y maintenir. +import { auHasard, consulterSite, lire, preparer } from './lib/api.js'; +import { STATISTIQUES } from './lib/config.js'; +import { rapport } from './lib/rapport.js'; + +export const options = { + insecureSkipTLSVerify: true, + scenarios: { + stress: { + executor: 'ramping-arrival-rate', + startRate: 5, + timeUnit: '1s', + preAllocatedVUs: 50, + maxVUs: 400, + stages: [ + { duration: '2m', target: 25 }, + { duration: '2m', target: 50 }, + { duration: '2m', target: 100 }, + { duration: '2m', target: 200 }, + { duration: '1m', target: 0 }, + ], + }, + }, + thresholds: { + http_req_failed: [{ threshold: 'rate<0.10', abortOnFail: true, delayAbortEval: '30s' }], + 'http_req_duration{type:lecture}': ['p(95)<2000'], + }, + summaryTrendStats: STATISTIQUES, +}; + +export const setup = preparer; + +export default function (data) { + if (Math.random() < 0.6) { + lire('/stats/summary', 'stats_summary', data.jeton); + } else { + consulterSite(auHasard(data.sites), data.jeton); + } +} + +export function handleSummary(data) { + return rapport('stress', data); +}