JMeter¶
PerfShop intègre Apache JMeter sous la forme de deux services Docker complémentaires qui forment ensemble une plateforme de tir de charge pilotable depuis le navigateur :
perfshop-jmeter— un containerjustb4/jmeter:5.5qui reste en permanence idle. Il ne démarre jamais de tir tout seul ; il attend qu'un ordre d'exécution lui parvienne viadocker exec.perfshop-jmeter-ui— une application Node.js / Express qui fournit une interface web de pilotage : parcours d'arborescence des scénarios, éditeur JMX, lancement de tirs, streaming des logs, récupération des rapports HTML, et exposition des métriques live.
Le tandem permet à un étudiant de lancer un tir complet sans jamais ouvrir un terminal, tout en conservant la puissance et la compatibilité totale de JMeter standalone.
Source de vérité
Cette page est tirée des blocs perfshop-jmeter et perfshop-jmeter-ui des fichiers docker-compose.desktop.yml / docker-compose.build.yml, du contenu du dossier jmeter/ (scenarios/, scripts/, plugins/), et des 9 modules Node du dossier jmeter-ui/src/ (server.js, config.js, i18n.js, docker.js, security.js, auth.js, jmx-parser.js, scenarios.js, jmeter.js).
Architecture du tandem¶
flowchart TB
subgraph browser["Navigateur (formateur ou étudiant)"]
UI["UI JMeter<br/>http://localhost:3005"]
end
subgraph jmui["perfshop-jmeter-ui<br/>(Node 20, Express, port 3005)"]
direction TB
SRV["server.js"]
AU["auth.js<br/>(session 8h + CSRF + rate limit)"]
SC["scenarios.js<br/>(arborescence, CRUD)"]
JP["jmx-parser.js<br/>(édition des .jmx)"]
JM["jmeter.js<br/>(lancement + status + logs)"]
DK["docker.js<br/>(socket Docker)"]
SRV --> AU
SRV --> SC
SRV --> JP
SRV --> JM
JM --> DK
end
subgraph jm["perfshop-jmeter<br/>(justb4/jmeter:5.5, idle)"]
ENT["entrypoint:<br/>cp /plugins/*.jar ... && tail -f /dev/null"]
JAR["jmeter.jar"]
PP[/"Prometheus plugin<br/>:9270 (pendant un tir)"/]
ENT --- JAR
JAR --- PP
end
subgraph vols["Volumes partagés<br/>(bind mounts)"]
SCEN["./jmeter/scenarios"]
SCRIPTS["./jmeter/scripts"]
RESULTS["./jmeter/results"]
PLUGS["./jmeter/plugins"]
LOGS["./jmeter/logs"]
end
subgraph obs["Observabilité"]
PROM["prometheus<br/>(job jmeter, scrape :9270)"]
LOKI["loki<br/>(tail jmeter.log)"]
GRAF["grafana<br/>(dashboard perfshop-jmeter-live)"]
end
DOCK[/"/var/run/docker.sock"/]
UI -->|HTTPS| SRV
DK -->|monte| DOCK
DOCK -.->|docker exec perfshop-jmeter<br/>jmeter -n -t ... -l ...| JAR
jm --- vols
jmui --- vols
PROM -->|scrape 5s| PP
LOKI -->|tail| LOGS
PROM --> GRAF
LOKI --> GRAF
Le point clé : perfshop-jmeter-ui ne lance pas JMeter dans son propre container. Il exploite le socket Docker monté en bind mount pour exécuter docker exec perfshop-jmeter jmeter -n -t .... Cela garantit que les tirs tournent dans l'image JMeter officielle, avec toutes ses dépendances et son classpath déjà configurés, et sans pénaliser les performances de l'UI avec la JVM JMeter.
perfshop-jmeter — container permanent idle¶
Configuration Docker Compose¶
| Clé | Valeur |
|---|---|
| Image | justb4/jmeter:5.5 (variable JMETER_IMAGE) |
container_name |
perfshop-jmeter |
| Réseau | perfshop-network |
restart |
unless-stopped |
| Entrypoint | /bin/sh -c "cp /plugins/*.jar /opt/apache-jmeter-5.5/lib/ext/ 2>/dev/null \|\| true && echo '[JMeter] Plugins installés' && tail -f /dev/null" |
Pourquoi l'entrypoint tail -f /dev/null ?¶
L'image justb4/jmeter:5.5 est conçue pour exécuter un seul tir puis se terminer — c'est son usage classique en CI. PerfShop a besoin de l'inverse : un container qui reste démarré en permanence pour qu'on puisse :
- Copier les plugins une seule fois au démarrage (via
cp /plugins/*.jar ...) - Lancer des tirs à la demande via
docker execsans payer le coût d'undocker runcomplet (téléchargement d'image, création de container, montage des volumes) - Conserver l'état entre les tirs (fichiers temporaires, logs agrégés)
Le tail -f /dev/null est l'astuce standard pour garder un container Docker vivant indéfiniment sans consommer de CPU.
Plugins copiés au démarrage¶
Le dossier ./jmeter/plugins est bind-mounté sur /plugins et contient un plugin externe :
| Plugin | Fichier | Rôle |
|---|---|---|
| JMeter Prometheus Listener | jmeter-prometheus-plugin-0.6.0.jar |
Expose les métriques d'un tir JMeter en cours sur un endpoint HTTP Prometheus (port 9270 par défaut) |
Le plugin est copié dans /opt/apache-jmeter-5.5/lib/ext/ au démarrage du container. Une fois le container en place, JMeter charge automatiquement le plugin au lancement de chaque tir — il devient utilisable dans les scénarios via l'élément Prometheus Listener côté JMeter.
Volumes bind-mountés¶
| Bind mount | Point de montage | Rôle |
|---|---|---|
./jmeter/scenarios |
/scenarios |
Fichiers JMX de scénarios (sources versionnables) |
./jmeter/scripts |
/scripts |
Scripts Groovy/Java utilisés par certains samplers |
./jmeter/results |
/results |
Sorties des tirs (fichiers CSV/JTL + rapports HTML) |
./jmeter/plugins |
/plugins |
Plugins externes à injecter dans lib/ext/ |
./jmeter/logs |
/jmeter-logs |
Logs JMeter (lus par Promtail pour Loki) |
Les mêmes dossiers sont aussi montés côté perfshop-jmeter-ui (sous /app/scenarios, /app/scripts, /app/results, /app/plugins) pour que l'UI puisse lire et écrire les mêmes fichiers que JMeter.
Variables d'environnement¶
| Variable | Valeur par défaut | Rôle |
|---|---|---|
HEAP |
-Xms256m -Xmx${JMETER_MAX_RAM:-512}m -XX:MaxMetaspaceSize=128m |
Options JVM passées à JMeter |
JMETER_CUSTOM_PLUGINS_FOLDER |
/plugins |
Dossier scanné par JMeter pour les plugins supplémentaires |
Dimensionnement de la heap JMeter
La variable JMETER_MAX_RAM contrôle le -Xmx de JMeter. 512 Mo suffisent pour un tir de quelques centaines de threads virtuels ; au-delà, il faut monter cette valeur dans le .env avant le up -d pour éviter les OutOfMemoryError côté injecteur (qui seraient alors injustement attribués au SUT).
Catalogue de scénarios livrés¶
Le dossier ./jmeter/scenarios contient plusieurs fichiers JMX prêts à l'emploi,
organisés en sous-dossiers thématiques :
| Scénario | vUsers max recommandés | Usage |
|---|---|---|
simple-api-products.jmx |
— | Tir minimal sur l'API produits — exemple et smoke test |
baseline-nominal.jmx |
— | Tir de référence nominal avant injection de chaos |
parcours-metier-nominal.jmx |
500 | Parcours métier complet T01–T10 (login → commande) sans chaos scripting |
E2E/parcours-complet-e2e.jmx |
500 | Parcours end-to-end complet, durée minimale 2h |
Fonctionnel/ |
— | Scénarios de validation fonctionnelle courts (sans montée en charge) |
Master/ |
— | Scénarios maîtres combinant plusieurs modules via Test Fragments |
Capacité de charge
Les scénarios parcours-metier-nominal.jmx et E2E/parcours-complet-e2e.jmx supportent
jusqu'à 500 vUsers simultanés grâce aux 500 comptes de charge dédiés (load1–load500).
Au-delà de 100 vUsers, penser à ajuster JMETER_MAX_RAM dans le .env.
Fichier de données utilisateurs — jmeter/scripts/users.csv¶
Le fichier users.csv est le jeu de données d'authentification partagé par tous les scénarios
qui font appel à POST /api/auth/login. Il est monté dans le container JMeter sous /scripts/users.csv.
Format (501 lignes : 1 header + 500 données) :
userEmail,userPassword,userName
load1@perfshop.com,LoadTest1!,Load User1
load2@perfshop.com,LoadTest2!,Load User2
...
load500@perfshop.com,LoadTest500!,Load User500
Paramétrage JMeter dans les JMX :
| Propriété CSVDataSet | Valeur | Raison |
|---|---|---|
filename |
/scripts/users.csv |
Chemin Docker (bind mount ./jmeter/scripts → /scripts) |
shareMode |
shareMode.all |
Tous les threads partagent un pointeur unique — chaque thread reçoit un compte distinct, zéro collision |
ignoreFirstLine |
true |
Saute le header userEmail,userPassword,userName |
recycle |
true |
Repart au début une fois les 500 lignes épuisées |
stopThread |
false |
Un thread ne s'arrête jamais faute de compte disponible |
shareMode.all vs shareMode.thread
Avec shareMode.thread (ancienne valeur), chaque thread relisait le CSV depuis le début
indépendamment — deux threads pouvaient utiliser le même compte simultanément, causant des
erreurs de cooldown anti-doublon côté backend. Avec shareMode.all, le pointeur avance
séquentiellement pour l'ensemble des threads : account N → thread N, sans conflit.
Source des comptes : migration Flyway V2__data_users.sql (présente dans migration-fr/
et migration-en/). Les mots de passe sont hashés BCrypt strength 10 en base (
préfixe $2b$10$). Le CSV contient les mots de passe en clair — nécessaire pour que
JMeter puisse les envoyer au POST /api/auth/login qui effectue la comparaison BCrypt côté backend.
perfshop-jmeter-ui — Express Node.js¶
Configuration Docker Compose¶
| Clé | Valeur |
|---|---|
| Image de base | node:20.19-alpine |
working_dir |
/app |
command |
sh -c "npm install --omit=dev && node src/server.js" |
| Port exposé | 3005 (hôte) → 3005 (container), variable JMETER_UI_PORT |
L'image n'est pas buildée à partir d'un Dockerfile : le code source est monté en bind mount (./jmeter-ui:/app) et npm install tourne au démarrage. Les node_modules sont persistés dans un volume nommé (jmeter-ui-modules:/app/node_modules) pour ne pas être perdus à chaque redémarrage.
Bind mounts critiques¶
| Bind mount | Point de montage | Rôle |
|---|---|---|
./jmeter-ui |
/app |
Code source Node.js (hot-reload en édition) |
./jmeter/scripts |
/app/scripts |
Lecture des scripts Groovy |
./jmeter/scenarios |
/app/scenarios |
Lecture/écriture des fichiers JMX |
./jmeter/results |
/app/results |
Lecture des rapports et des fichiers JTL |
./jmeter/plugins |
/app/plugins |
Lecture de la liste des plugins |
/var/run/docker.sock |
/var/run/docker.sock |
Accès à l'API Docker pour docker exec |
jmeter-ui-modules (volume nommé) |
/app/node_modules |
Cache des dépendances Node |
Socket Docker : accès complet au démon
Le bind mount /var/run/docker.sock donne à perfshop-jmeter-ui un accès complet au démon Docker de l'hôte. Ce choix est assumé pour permettre le pattern docker exec sans complexité réseau supplémentaire, mais il impose de ne jamais exposer directement ce service sur internet. Un reverse proxy avec auth forte et restriction IP est indispensable pour une exposition publique.
Variables d'environnement¶
| Variable | Défaut | Rôle |
|---|---|---|
PORT / SCRIPTS_UI_PORT |
3005 |
Port HTTP du serveur Node |
PUBLIC_JMETER_URL |
http://localhost:3005 |
URL publique exposée au navigateur |
JMETER_TARGET_INTERNAL |
http://perfshop-app:8080 |
Cible « backend direct » (bypasse nginx) |
JMETER_TARGET_EXTERNAL |
PUBLIC_API_URL |
Cible publique équivalente côté reverse proxy |
JMETER_TARGET_FRONTEND |
PUBLIC_FRONTEND_URL |
Cible frontend (pour tirs front) |
JMETER_PROMETHEUS_PORT |
9270 |
Port exposé par le plugin Prometheus côté JMeter |
PROMETHEUS_INTERNAL_URL |
http://perfshop-prometheus:9090 |
URL interne de Prometheus pour requêter les métriques live |
PERFSHOP_API_INTERNAL |
(obligatoire, fail-fast) | URL interne du backend pour healthchecks |
JMETER_CONTAINER_NAME |
perfshop-jmeter |
Nom du container cible pour docker exec |
JMETER_MAX_RAM |
512 (ou 1024 selon cible) |
Heap max transmise à JMeter |
GRAFANA_URL |
PUBLIC_GRAFANA_URL |
Lien profond vers le dashboard perfshop-jmeter-live depuis l'UI |
PERFSHOP_LANG |
fr ou en |
Langue de l'UI et des traductions serveur |
SESSION_SECRET |
perfshop-jmeter-ui-secret (dev) |
Secret de signature des cookies de session — fail-fast si laissé à la valeur par défaut avec NODE_ENV=production |
Les 9 modules du dossier src/¶
| Module | Lignes | Rôle |
|---|---|---|
server.js |
103 | Orchestration Express, session, middlewares, montage des routes, injection window.__CONFIG__ via marker HTML |
config.js |
51 | Lecture et validation des variables d'environnement au démarrage |
i18n.js |
51 | Chargement des bundles de traduction depuis public/i18n/{fr,en}.json |
docker.js |
94 | Client bas niveau pour l'API Docker (mode Detach:true) via le socket Unix |
security.js |
47 | Middlewares csrfProtection et rateLimit(windowMs, maxHits) |
auth.js |
68 | Login/logout/status + middleware requireAuth + liste PUBLIC_PATHS |
jmx-parser.js |
137 | Parsing et édition des fichiers JMX (structure XML) |
scenarios.js |
218 | Arborescence, CRUD, upload, édition des scénarios |
jmeter.js |
548 | Lancement, stop, status, logs, résultats, métriques live (module central) |
Injection de la configuration dans la page¶
Le serveur expose la configuration navigateur via un marker HTML stable placé dans public/index.html :
Au servage de /, le serveur :
- Lit
public/index.html - Cherche le marker
<!-- @CONFIG_INJECT@ --> - Le remplace par
<script>window.__CONFIG__ = {...};</script> - En cas de marker absent, log une erreur et fait une injection de fallback avant
</head>
Ce mécanisme évite le double chargement config + HTML et garantit que le JavaScript côté navigateur a accès à PUBLIC_CONFIG avant tout autre script.
Endpoints REST exposés¶
| Méthode | Route | Module | Auth | Rôle |
|---|---|---|---|---|
POST |
/api/auth/login |
auth.js | public (rate-limité) | Authentification |
POST |
/api/auth/logout |
auth.js | session | Fermeture de session |
GET |
/api/auth/status |
auth.js | public | État de la session courante |
GET |
/api/scenarios/tree |
scenarios.js | session | Arborescence complète des JMX |
GET |
/api/scenarios/:path |
scenarios.js | session | Lecture d'un JMX |
POST |
/api/scenarios/upload |
scenarios.js | session | Upload d'un JMX |
PUT |
/api/scenarios/:path |
scenarios.js | session | Modification d'un JMX (via jmx-parser) |
POST |
/api/run |
jmeter.js | session | Lance un tir (docker exec perfshop-jmeter jmeter -n -t ...) |
POST |
/api/stop |
jmeter.js | session | Stoppe le tir courant (stoptest.sh puis SIGTERM) |
GET |
/api/status |
jmeter.js | session | État courant (running, PID, temps écoulé, JMX) |
GET |
/api/logs |
jmeter.js | session | Streaming des dernières lignes de jmeter.log |
GET |
/api/results |
jmeter.js | session | Liste des rapports disponibles dans /results |
GET |
/api/results/:id |
jmeter.js | session | Détail d'un rapport |
DELETE |
/api/results/:id |
jmeter.js | session | Suppression d'un rapport |
GET |
/api/metrics-live |
jmeter.js | session | Interroge Prometheus et renvoie les métriques live du tir en cours |
GET |
/api/jmeter-container/status |
jmeter.js | public | Status du container perfshop-jmeter (UP/DOWN) |
GET |
/api/jmeter/status |
jmeter.js | public | Alias public pour monitoring externe |
Sécurité¶
Trois couches successives protègent l'UI :
flowchart LR
REQ["Requête HTTP"]
REQ --> S1["CSRF protection<br/>(global)"]
S1 --> S2["Rate limiting<br/>(POST /api/auth/login<br/>10 tentatives / min / IP)"]
S2 --> S3["Auth gate<br/>(apiAuthGate sur /api/*<br/>sauf PUBLIC_PATHS)"]
S3 --> S4["requireAuth<br/>(middleware par route)"]
S4 --> H["Handler"]
| Couche | Détail |
|---|---|
| CSRF | Token généré par session, vérifié sur toutes les requêtes mutantes (POST, PUT, DELETE) |
| Rate limit | rateLimit(60_000, 10) : 10 tentatives de login par minute par IP, puis HTTP 429 |
| Session | express-session 8 heures, cookie httpOnly, secure conditionnel à HTTPS, sameSite lax en HTTP et none en HTTPS cross-site |
| Auth gate | apiAuthGate sur /api/* avec liste PUBLIC_PATHS explicite (/api/auth/*, /api/jmeter/status, /api/jmeter-container/status) |
| requireAuth | Middleware systématique par route sensible — double filet de sécurité même si l'auth gate est mal configurée |
Récupération d'état au redémarrage¶
Au démarrage, server.js appelle await recoverRunState() (défini dans jmeter.js, ligne 520). Cette fonction :
- Lit un fichier de persistance qui mémorise l'état du dernier tir en cours
- Si un tir était actif au crash du container, interroge Docker pour vérifier s'il tourne encore dans
perfshop-jmeter - Si oui, rebranche l'UI sur ce tir (l'étudiant retrouve son tir en cours après un redémarrage du service UI)
- Si non, marque le tir comme terminé de manière propre
C'est le pattern standard de résilience cross-container : l'état vivant (le tir) est dans un container, l'état métadonnées (qui, quand, quel JMX) est dans l'autre, et la synchronisation est faite au démarrage.
Flux d'un tir complet¶
sequenceDiagram
autonumber
actor F as Formateur
participant UI as Navigateur<br/>(jmeter-ui)
participant SRV as perfshop-jmeter-ui<br/>(Node)
participant SOCK as /var/run/docker.sock
participant JM as perfshop-jmeter<br/>(idle)
participant PROM as prometheus
participant LOKI as loki
participant GRAF as grafana
F->>UI: Login (email/mdp)
UI->>SRV: POST /api/auth/login
SRV-->>UI: 200 + cookie session
F->>UI: Sélectionne scenario.jmx
UI->>SRV: GET /api/scenarios/tree
SRV-->>UI: arborescence JMX
F->>UI: Clique « Lancer »
UI->>SRV: POST /api/run {jmx: "Master/baseline.jmx"}
SRV->>SOCK: docker exec perfshop-jmeter jmeter -n -t /scenarios/... -l /results/...
SOCK->>JM: exec
JM->>JM: Démarre le tir
JM->>PROM: expose :9270/metrics (plugin Prometheus)
loop Pendant le tir
UI->>SRV: GET /api/status (polling)
SRV-->>UI: {running: true, elapsed, ...}
UI->>SRV: GET /api/metrics-live
SRV->>PROM: PromQL sur jmeter_*
PROM-->>SRV: métriques
SRV-->>UI: {tps, latency_p95, errors, ...}
UI->>SRV: GET /api/logs
SRV->>JM: docker logs --tail 50 (via socket)
JM-->>SRV: lignes log
SRV-->>UI: logs récents
end
Note over JM,LOKI: En parallèle, Promtail lit /jmeter-logs/jmeter.log<br/>et le pousse dans Loki (job static_configs)
JM->>JM: Fin du tir, rapport HTML généré dans /results
UI->>SRV: GET /api/results
SRV-->>UI: liste des rapports
F->>UI: Clique sur le rapport → ouvre le HTML dans un iframe
F->>GRAF: Ouvre le dashboard perfshop-jmeter-live
GRAF->>PROM: PromQL (jmeter_*)
GRAF->>LOKI: LogQL ({job="jmeter-log"})
Intégration observabilité¶
Métriques Prometheus (pendant un tir)¶
Le plugin JMeter Prometheus Listener expose, pour toute la durée d'un tir, un endpoint HTTP :9270/metrics à l'intérieur du container perfshop-jmeter. Prometheus scrape ce job toutes les 5 secondes avec honor_labels: true, ce qui permet au plugin de définir lui-même ses labels de ventilation (par sampler name, par code HTTP, etc.) sans collision avec les labels de scrape.
Le dashboard Grafana perfshop-jmeter-live (22 panels mixant Prometheus et Loki) consomme directement ces métriques ; détails dans ../observability/dashboards.md.
Logs Loki¶
Le fichier ./jmeter/logs/jmeter.log est lu par Promtail via un job static_configs dédié. L'étiquette Loki est {job="jmeter-log"} et permet au dashboard jmeter-live d'afficher en temps réel les dernières lignes du log à côté des courbes de latence. Voir ../observability/loki.md.
Logs Vector → OpenSearch¶
Vector collecte également les logs Docker de perfshop-jmeter et les indexe dans OpenSearch sous perfshop-jmeter* via la règle de routage service_family = "jmeter" (vector.toml). Voir ../observability/opensearch.md.
Pour aller plus loin¶
- Vue d'ensemble QA
- Scripts UI — le même pattern Node + bind socket, mais pour piloter
perfshop-test-runner perfshop-jmeter-livedashboard- Prometheus et PromQL JMeter