Session 20 — JMeter UI : debug NAS, métriques Prometheus, IHM avancée¶
Durée : ~12 heures
Contexte : Session marathon de déploiement, debug, et enrichissement de l'IHM JMeter
sur le NAS Synology. Résolution de 25+ bugs de déploiement, intégration du plugin Prometheus
johrstrom, puis ajout des fonctionnalités avancées de l'IHM (graphiques, gestionnaire
scénarios, éditeur JMX inline, logs en direct).
🎯 Ce qui a été produit¶
1. Déploiement sur NAS Synology — problèmes et résolutions¶
Problème seccomp : apk add docker-cli dans le Dockerfile plante sur le NAS (kernel DSM 7
sans seccomp). Solution : suppression de docker-cli, pilotage Docker exclusivement via
l'API socket Unix (/var/run/docker.sock) en Node.js pur.
Architecture retenue — sans docker-cli :
Node.js jmeter-ui
└── socket /var/run/docker.sock
├── POST /containers/exec → lancer JMeter (docker exec dans container permanent)
├── GET /containers/json → détecter fin de tir
└── GET /containers/{id}/logs → logs perfshop-jmeter-ui
Mode volume vs build : Sur le NAS, Dockerfile inutilisable (npm install planterait aussi).
Solution dans docker-compose NAS : image: node:20-alpine + code monté en volume + npm install
lancé à chaque démarrage dans la command:.
2. Container JMeter permanent (exit seccomp → docker exec)¶
Abandon du container éphémère créé par docker run à chaque tir — trop de problèmes
(réseau introuvable, image absente, args mal formés). Migration vers un container permanent
perfshop-jmeter avec tail -f /dev/null, et exécution des tirs via docker exec.
| Avant | Après |
|---|---|
| Container éphémère créé à chaque tir | Container permanent, exec au lancement |
| Démarrage JVM ~5s par tir | JVM déjà up, démarrage quasi-instantané |
| Problèmes réseau Docker | Réseau fixe dans compose |
HOST_WORKDIR pour bind-mounts |
Volumes fixés dans compose |
Variables variabilisées : JMETER_IMAGE, JMETER_CONTAINER_NAME, DOCKER_NETWORK,
JMETER_TARGET_INTERNAL, JMETER_TARGET_EXTERNAL, JMETER_TARGET_FRONTEND — toutes
issues du .env, jamais hardcodées.
3. Plugin Prometheus johrstrom — intégration complète¶
17 bugs résolus pour faire fonctionner le pipeline complet :
JMeter → PrometheusListener :9270 → Prometheus scrape → IHM métriques live
Points clés résolus :
prometheus.ip=0.0.0.0obligatoire (défaut 127.0.0.1 → inaccessible depuis Prometheus)PrometheusListenerdoit être dans lehashTreedu ThreadGroup (pas au niveau TestPlan)- Les
stringProp namedes labels doivent utiliser les hashcodes Java ("102727412","3059181") et non des noms arbitraires prometheus.yml: jobjmeterciblantperfshop-jmeter:9270+--web.enable-lifecycledans Prometheus pour rechargement à chaud- Assertions JMX :
test_type=8(Equals sur response_code) obligatoire —test_type=2cherchait dans le body, pas le code HTTP
Métriques disponibles confirmées sur le NAS :
jmeter_response_time{quantile="0.5|0.95|0.99", label="GET /api/products"}
jmeter_transactions_total{label="..."}
jmeter_success_ratio_failure / _success / _total
jmeter_threads{state="active|started|finished"}
jmeter_response_time_count / _sum
4. Corrections des 7 scénarios JMX¶
| Fichier | Corrections |
|---|---|
simple-api-products.jmx |
ConstantTimer 5s → GaussianRandomTimer ~800ms ±300ms |
baseline-nominal.jmx |
Port 9080→8080, PrometheusListener dans ThreadGroup |
browse-product-f4.jmx |
Assertion test_type 2→8, PrometheusListener dans TG |
checkout-stress-f1f2.jmx |
2 assertions corrigées, PrometheusListener dans TG |
load-catalogue-f3.jmx |
Assertion OR test_type 6→40, PrometheusListener dans TG |
full-chaos-master.jmx |
PrometheusListener dans TG |
parcours-complet-e2e.jmx |
8 assertions corrigées, PrometheusListener dans TG |
Règle définitive pour les assertions :
- Code unique → test_type=8 + Assertion.test_field=Assertion.response_code
- Codes multiples (OR) → test_type=40 + Assertion.test_field=Assertion.response_code
Organisation en sous-dossiers (analogie LoadRunner) :
jmeter/scenarios/
├── baseline-nominal.jmx → Général
├── simple-api-products.jmx
├── Fonctionnel/ → 3 scénarios
├── Master/ → 1 scénario
└── E2E/ → 1 scénario
5. resolveDockerTarget — localhost → host.docker.internal¶
Sur Docker Desktop Windows, localhost depuis un container = le container lui-même,
pas la machine hôte. Solution dans server.js : substitution automatique.
// server.js — resolveDockerTarget()
const resolvedTarget = rawTarget.replace(/localhost/g, 'host.docker.internal');
Transparent pour l'étudiant : il voit localhost:9080, JMeter reçoit host.docker.internal:9080.
6. IHM avancée — fonctionnalités ajoutées¶
Sélecteur URL cible enrichi :
- 4 modes radio : Interne / Externe / Frontend / Personnalisée
- Aperçu dynamique URL utilisée : mis à jour en temps réel
- Checkbox "Utiliser le JMX tel quel" — désactive la surcharge -J params pour les étudiants
qui ont codé leurs URLs en dur dans leur script
Sélecteur vUsers : radio Slider (1–500) ↔ Saisie libre (illimité) avec grisage croisé
Sélecteur Durée : radio Slider (max 24h) ↔ Saisie libre (valeur + unité min/h/j/sem)
avec badge › 24h quand le slider n'est plus représentatif. Source de vérité = _durationMinReal.
Gestionnaire de scénarios (modale) : - Vue arborescente des dossiers + fichiers - Drag & drop entre dossiers - Création de dossier - Upload vers le dossier choisi - Suppression fichiers et dossiers vides
Éditeur JMX inline (modale) :
- Parse XML côté serveur → extraction ThreadGroups, timers, assertions, samplers
- Bandeau "Variables disponibles" avec ${__P(users,...)} et ${__P(duration,...)} copiables
- Champs éditables : threads, ramp-up, durée, boucles, think times
- Bouton ↩ Reset par champ + badge orange si valeur diverge de l'original
- Sauvegarde → réécriture XML du JMX
Badge container JMeter (header) : - Polling toutes les 60s via socket Docker - 3 états : 🟢 JMeter ✓ / 🔴 JMeter ✗ / ⚪ Vérification - Tooltip : nom container, statut, heure démarrage
Badge paramètres du tir : 👥 110 vUsers · ⏱ 07:00 dans le bandeau "En cours"
pour distinguer les paramètres lancés des valeurs courantes de l'IHM.
Persistance runState : /app/results/.runstate.json — survit aux restarts Node.
Au redémarrage : récupération du tir en cours si JMeter tourne encore, sinon finished.
Grisage config pendant tir : tous les contrôles de configuration grisés (opacity:0.45,
pointer-events:none, disabled) pendant qu'un tir est en cours. Seul le bouton ⏹ reste actif.
Gestion exit codes : | Code | Signification | Badge IHM | |---|---|---| | 0 | Succès complet | 🟣 Terminé | | 1 | Assertions échouées (chaos normal) | 🟡 Terminé (erreurs sample) | | 143 | SIGTERM — fin durée ou bouton Stop | 🟣 Terminé | | Autre | Crash JMeter | 🔴 Erreur |
7. Logs JMeter — double accès¶
IHM /api/logs : lecture via socket Docker
- docker exec perfshop-jmeter tail -n N /jmeter-logs/jmeter.log pour les logs JMeter
- docker logs --tail N perfshop-jmeter-ui pour les logs Node.js
- Panneau deux colonnes avec sélecteurs target + nombre de lignes + auto-refresh 5s
- Bouton 🔄 par tableau pour actualisation individuelle
- Gestion 401 : stop auto + reload page login si session expirée
Grafana/Loki :
- perfshop-jmeter-ui : collecté par Promtail via docker_sd ✅
- perfshop-jmeter : collecté via fichier statique Promtail depuis ./jmeter/logs/jmeter.log
(volume ./jmeter/logs:/jmeter-logs monté sur le container ET sur Promtail)
- Commande JMeter : -j /jmeter-logs/jmeter.log pour écrire dans le volume partagé
Dashboard Grafana JMeter — 3 nouveaux panels (IDs 105-107) :
- Row "📋 Logs — JMeter & UI"
- Panel logs perfshop-jmeter (Loki query {container="perfshop-jmeter"})
- Panel logs perfshop-jmeter-ui (Loki query {container="perfshop-jmeter-ui"})
8. Dashboard Grafana JMeter — mise à jour complète¶
Réécriture depuis zéro avec les vraies métriques johrstrom (ancien dashboard utilisait des noms Influx incorrects). 25 panels en 4 sections :
| Section | Panels |
|---|---|
| Stats KPI | vUsers actifs, TPS, Taux erreur %, P95, Total samples, Temps moyen |
| Temps de réponse | P50/P95/P99/Moy global + par label (sampler) |
| Débit & Erreurs | TPS total + par label, taux succès/erreur %, transactions bargauge |
| Threads | actifs/démarrés/terminés + samples par label |
| Corrélation backend | Heap JVM, erreurs HTTP, latence Spring, CPU process |
| Logs | perfshop-jmeter + perfshop-jmeter-ui (Loki) |
Variable de template $rate_window (15s/30s/1m/2m) ajoutée pour ajuster les rate().
9. Rapport HTML JMeter et gestion des rapports IHM¶
- Rapports servis statiquement depuis
/report/run-XXXXX-report/index.html - Liste des rapports avec date/heure lisible extraite du timestamp dans le nom de fichier
- Bouton 🗑 par rapport avec confirmation → suppression du dossier
-report/+.jtl - Validation côté serveur : regex
^run-\d+-report$(anti path traversal)
10. Guide pédagogique MkDocs¶
Nouveau fichier mkdocs/docs/guides/jmeter-script-custom.md :
- Procédure complète pour créer un scénario JMeter depuis l'IHM JMeter GUI
- Comment configurer les variables ${__P(target_host,...)} / ${__P(target_port,8080)}
- Comment ajouter le PrometheusListener (position correcte = enfant du ThreadGroup)
- Comment uploader et lancer depuis l'IHM PerfShop
- Checklist 6 points de contrôle avant upload
📦 Fichiers modifiés¶
| Fichier | Nature |
|---|---|
jmeter-ui/src/server.js |
Réécriture complète — socket Docker, exec permanent, logs, éditeur JMX |
jmeter-ui/public/index.html |
IHM avancée — graphiques, gestionnaire, éditeur, logs, grisage |
jmeter-ui/Dockerfile |
Suppression docker-cli (seccomp NAS incompatible) |
jmeter-ui/package.json |
Dépendances nettoyées (suppression cors, multer mis à jour) |
docker-compose.yml |
Container JMeter permanent, volumes logs, mode volume NAS |
docker-compose.desktop.yml |
Alignement avec NAS, resolveDockerTarget, volumes logs |
docker-compose.build.yml |
Alignement complet (Dockerfile build conservé) |
.env.example + .env.production |
Variables JMeter complètes (image, réseau, cibles, RAM) |
prometheus/prometheus.yml |
Job jmeter → perfshop-jmeter:9270, --web.enable-lifecycle |
grafana/dashboards/dashboard-jmeter.json |
25+3 panels, métriques johrstrom correctes |
promtail/promtail-config.yml |
perfshop-jmeter-ui dans docker_sd, job fichier jmeter.log |
jmeter/scenarios/*.jmx (7 fichiers) |
Assertions corrigées, PrometheusListener dans TG |
mkdocs/docs/guides/jmeter-script-custom.md |
Guide créer/uploader un scénario custom |
mkdocs/docs/roadmap/roadmap.md |
Phase 8b JMeter UI documentée, Phase 9.3 = ✅ Livré |
mkdocs/docs/vibe-coding/session-17.md |
Créé |
mkdocs/docs/vibe-coding/session-18.md |
Créé |
mkdocs/docs/vibe-coding/session-19.md |
Créé |
🏗️ Anti-patterns documentés (leçons de déploiement)¶
| Anti-pattern | Correction |
|---|---|
apk add docker-cli sur NAS |
Socket Docker Unix uniquement |
docker run container éphémère |
docker exec dans container permanent |
require() dans module ESM |
import { spawn } from 'child_process' |
| Port hardcodé en dur | Toujours via variables .env |
PrometheusListener au niveau TestPlan |
Doit être enfant du ThreadGroup |
test_type=2 pour assertion HTTP |
test_type=8 (Equals) ou test_type=40 (OR) |
prometheus.ip par défaut |
Toujours -Jprometheus.ip=0.0.0.0 |
localhost dans URL cible JMeter |
resolveDockerTarget() → host.docker.internal |
🎓 Valeur pédagogique¶
| Concept | Ce que l'apprenant voit |
|---|---|
| Container JMeter permanent | Pas de latence JVM, tir quasi-instantané |
| Métriques Prometheus live | P50/P95/P99 en temps réel pendant le tir |
| Sous-dossiers scénarios | Organisation par domaine comme LoadRunner |
| Éditeur JMX inline | Modifier threads/durée/timers sans ouvrir JMeter GUI |
| Logs en direct | Voir les samplers JMeter et logs Node.js sans SSH |
| Chaos + assertions | Exit code 1 = assertions échouées par chaos = comportement normal |