@mostajs/mjs-metrics
v0.3.1
Published
Métriques & test de charge façon Apache JMeter pour l'écosystème @mostajs/* : scénarios HTTP, montée en charge, percentiles (p50/p90/p99) via t-digest zéro-dépendance, débit, taux d'erreur, seuils. Bâti sur le client HTTP de @mostajs/mjs-unit/web.
Maintainers
Readme
@mostajs/mjs-metrics
Auteur : Dr Hamid MADANI [email protected] · Licence : AGPL-3.0-or-later Statut : ✅ 0.2.0 — implémenté, testé (17 tests verts via mjs-unit), zéro dépendance runtime.
Métriques et test de charge façon Apache JMeter pour l'écosystème @mostajs/*.
On décrit des scénarios (suites de requêtes HTTP), on les exécute sous montée
en charge (utilisateurs virtuels concurrents, paliers, durée), et on obtient un
rapport de performance : débit (req/s), latences p50/p90/p99 (t-digest
interne), taux d'erreur, min/max/moy, seuils de réussite, et une preuve HTML
autonome.
Bâti sur le client HTTP zéro-dépendance de @mostajs/mjs-unit/web
(createHttpTester / startServer) : aucune dépendance de transport ajoutée.
Installation
npm i -D @mostajs/mjs-metrics @mostajs/mjs-unit@mostajs/mjs-unit est un peer (fournit le client HTTP). En dev d'un module
@mostajs/*, les deux sont déjà dans le workspace.
Démarrage rapide
import { scenario, run } from "@mostajs/mjs-metrics";
const home = scenario("home")
.step("GET /", (http) => http.get("/"))
.check((res) => res.status === 200);
const report = await run(home, { target: "http://localhost:3000", vus: 25, duration: "15s" });
report.print(); // résumé console
if (!report.passed) process.exit(1); // CI : échec si un seuil casseFAQ — « je veux mesurer des valeurs quelconques dans mon application »
Question fréquente : comment introduire mjs-metrics pour suivre des flux à l'intérieur d'une app que je développe (pas la charger de l'extérieur) ?
Il y a deux usages distincts :
| Besoin | Outil |
|---|---|
| Charger depuis l'extérieur (envoyer du trafic, mesurer la latence HTTP) | scenario() + run() |
| Instrumenter des valeurs à l'intérieur de l'app (suivre un flux, chronométrer une fonction, compter des événements) | createMetrics() — voir Instrumentation in-app |
scenario().step(http => …) est couplé au HTTP (la step renvoie une
TestResponse, c'est piloté par vus/iterations) : ce n'est pas l'outil
pour du suivi interne continu. Pour ça, le registre @mostajs/mjs-metrics/runtime
te donne compteurs / jauges / histogrammes / chronomètres + snapshot() / toJSON()
/ toHTML() et un probe /metrics.
Besoin sur mesure ? La primitive TDigest (+ summarize) est exportée et
réutilisable seule, si tu veux construire ton propre agrégat :
import { TDigest, summarize } from "@mostajs/mjs-metrics";
import { performance } from "node:perf_hooks";
const traitementCmd = new TDigest(); // un histogramme par flux suivi
let nbCmd = 0, nbErreurs = 0;
async function traiterCommande(cmd) {
const t0 = performance.now();
try { await faireLeTravail(cmd); nbCmd++; }
catch (e) { nbErreurs++; throw e; }
finally { traitementCmd.add(performance.now() - t0); } // ← la valeur mesurée
}
export const snapshot = () => ({
commandes: nbCmd,
erreurs: nbErreurs,
latence_ms: summarize(traitementCmd), // { count, min, max, mean, p50, p90, p99 }
});digest.add(valeur) accepte n'importe quelle valeur numérique (durée, taille
de payload, profondeur de file, score…), pas seulement du temps.
How-to (recettes)
1. Tester un handler/app sans démarrer de serveur
target accepte un handler Node (req, res) => … (ou une app Express) : un serveur
éphémère est démarré pour toute la campagne puis fermé. Idéal en CI / micro-bench.
import { scenario, run } from "@mostajs/mjs-metrics";
import { createApp } from "../src/app.js"; // votre app : (req,res)=>… ou http.Server
const report = await run(
scenario("ping").step("GET /health", (http) => http.get("/health")).check((r) => r.status === 200),
{ target: createApp(), vus: 50, iterations: 100 }, // iterations = déterministe
);
report.print();2. Parcours multi-étapes avec vérifications et think time
const parcours = scenario("checkout")
.step("POST /login", (http) => http.post("/login", { user: "a", pass: "b" }))
.check((r) => r.status === 200)
.think(300) // pause "humaine" entre deux étapes
.step("GET /cart", (http) => http.get("/cart"))
.check((r) => Array.isArray(r.body.items));
await run(parcours, { target: "http://localhost:3000", vus: 30, duration: "1m" });Chaque .check() se rattache à la dernière étape ; un check faux compte la
requête comme un échec (alimente errorRate).
3. Montée en charge progressive (rampUp)
await run(home, {
target: "http://localhost:3000",
vus: 200,
rampUp: "20s", // les 200 VUs démarrent étalés sur 20 s
duration: "2m",
});4. Seuils → succès/échec (CI)
const report = await run(home, {
target: "http://localhost:3000",
vus: 50,
duration: "30s",
thresholds: {
p95: "<300", // latence p95 sous 300 ms
p99: "<800",
errorRate: "<=1%", // le % divise par 100
throughput: ">100", // au moins 100 req/s
},
});
console.log(report.passed ? "OK" : "KO", report.thresholds);Clés acceptées : p50 p90 p95 p99 min max mean errorRate throughput requests failures.
Opérateurs : < <= > >= ==.
5. Preuve HTML autonome
import { writeFileSync } from "node:fs";
const report = await run(home, { target: "http://localhost:3000", vus: 25, duration: "15s" });
writeFileSync("charge.html", report.toHTML({ title: "Charge — page d'accueil" }));Agréger plusieurs campagnes dans un seul rapport :
import { renderHtmlReport } from "@mostajs/mjs-metrics/report";
const a = await run(scenarioA, optsA);
const b = await run(scenarioB, optsB);
writeFileSync("rapport.html", renderHtmlReport([
{ label: "API publique", report: a },
{ label: "Back-office", report: b },
]));6. Lire les métriques par programme
const { metrics } = await run(home, { target, vus: 10, iterations: 50 });
console.log(metrics.throughput, metrics.errorRate, metrics.latency.p99);
console.log(metrics.perStep["GET /"].p90); // latences par étapeCLI
# scénario versionné + preuve HTML
mjs-metrics scenarios/login.mjs --vus 50 --duration 30s --rampup 5s --html=login.html
# plusieurs fichiers, sortie JSON agrégée
mjs-metrics scenarios/*.mjs --iterations 100 --json-out=rapport.json
# override de la cible depuis la ligne de commande
mjs-metrics scenarios/api.mjs --target http://staging.local --vus 20Chaque fichier exporte scenarios (et éventuellement options) :
// scenarios/login.mjs
import { scenario } from "@mostajs/mjs-metrics";
export const scenarios = scenario("login")
.step("POST /login", (http) => http.post("/login", { user: "a", pass: "b" }))
.check((r) => r.status === 200);
export const options = { target: "http://localhost:3000", vus: 50, thresholds: { p99: "<500" } };Les drapeaux CLI priment sur les options. Code de sortie 1 si un seuil casse.
Mini-démo
Le dépôt fournit une démo autonome (serveur simulé à latence variable + preuve HTML) :
node examples/mini-demo.mjs # → examples/mini-demo-report.html
mjs-metrics examples/cli-scenario.mjs --html=examples/cli-report.html
# instrumentation in-app : dashboard LIVE (recharger = chiffres frais)
bash examples/serve.sh # http://127.0.0.1:8088/metrics.html (ou : npm run demo)Tous les exemples sont décrits dans examples/README.md.
Extrait de sortie :
── mjs-metrics ─────────────────────────────
requêtes : 400 (échecs 0, 0.00%)
débit : 420.7 req/s sur 950.85 ms (10 VUs)
latence ms : min 2.62 · moy 12.39 · p50 12.14 · p90 18.92 · p99 31.82 · max 34.37
· POST /api/login: p50 15.51 · p90 20.05 · p99 33.44 (200)
· GET /api/profile: p50 9.06 · p90 13.99 · p99 15.95 (200)
seuils :
✓ p99 <200 (réel 31.82)
✓ errorRate <=0 (réel 0)
✓ throughput >50 (réel 420.68)
→ TOUS LES SEUILS PASSENTInstrumentation in-app (suivre des flux dans votre application)
Au-delà du load-testing, mjs-metrics expose un registre de métriques pour
mesurer des valeurs quelconques à l'intérieur de votre app — sous-export
@mostajs/mjs-metrics/runtime, zéro-dépendance (histogrammes/chronos via le
même t-digest).
import { createMetrics } from "@mostajs/mjs-metrics/runtime";
// Une fois, au démarrage de l'app.
export const m = createMetrics();
const commandes = m.counter("commandes"); // cumulatif
const file = m.gauge("file_attente"); // monte/descend
const taille = m.histogram("taille_panier"); // distribution d'une valeur
const chrono = m.timer("traitement_ms"); // durées (ms)Puis, là où vit le flux métier :
async function traiterCommande(cmd) {
file.inc();
try {
await chrono.measure(() => faireLeTravail(cmd)); // chronomètre auto
taille.observe(cmd.items.length);
commandes.inc();
} finally {
file.dec();
}
}Lecture à la demande :
m.snapshot(); // { counters, gauges, histograms:{p50,p90,p99,…}, timers:{…} }
m.toJSON(); // idem (endpoint /metrics)
m.toHTML(); // tableau de bord HTML autonomeExposer en HTTP (/metrics)
Le probe branche le registre sur une app Express ou un handler Node brut :
import { metricsProbe } from "@mostajs/mjs-metrics/runtime";
// Express
app.use(metricsProbe(m)); // GET /metrics (JSON) + /metrics.html (dashboard)
// http natif
const probe = metricsProbe(m, { title: "Service commandes" });
http.createServer((req, res) => {
if (probe(req, res)) return; // servi par le probe
// … votre routage …
}).listen(3000);Voir examples/runtime-inapp.mjs pour une démo complète (compteur/jauge/histo/chrono
- probe + HTML).
| Métrique | Méthodes | Pour |
|---|---|---|
| counter(n) | inc(k=1) add(k) reset() value | événements cumulés (requêtes, erreurs…) |
| gauge(n) | set(v) inc() dec() value | valeur instantanée (file, connexions…) |
| histogram(n) | observe(v) summary() count | distribution d'une valeur (tailles, scores…) |
| timer(n) | measure(fn) start()→stop() record(ms) summary() | durées (p50/p90/p99 en ms) |
Charge externe →
scenario()/run(). Suivi interne continu →createMetrics(). Même registre redemandé par son nom = même instance (partage entre modules).
API de référence
| Export | Rôle |
|---|---|
| scenario(name) | builder .step(label, fn).check(pred).think(ms) |
| run(scenarios, opts) | exécute la campagne → Promise<Report> |
| Report | { metrics, thresholds, passed, print(), toJSON(), toHTML(opts?) } |
| renderHtmlReport(input, opts?) | HTML autonome (1 rapport ou [{label, report}]) — via @mostajs/mjs-metrics/report |
| renderJsonReport(input) | JSON (1 rapport ou liste) |
| TDigest | estimateur de quantiles streaming, zéro-dépendance |
| summarize(digest) · parseDuration(str) | utilitaires |
RunOptions = { target, vus, rampUp?, duration?, iterations?, thresholds? }
(iterations rend la campagne déterministe et prime sur duration).
Tests
Suite exécutée par le CLI @mostajs/mjs-unit (zéro-dépendance) :
npm test # typecheck (tsc --noEmit) → build → node node_modules/.bin/mjs-unit test-scripts/*.test.mjs17 tests : t-digest, builder de scénario, orchestrateur run(), reporters HTML/JSON.
Limites
Mono-process : la charge réaliste est bornée par l'event-loop Node (très bien pour de l'in-process ou un service local ; pour des campagnes massives distribuées, viser une 0.3.0 multi-process).
