Introduction
Le monitoring transforme une API Express d'une boîte noire en un service fiable. Dans ce guide, vous allez :
- Ajouter des métriques et logs de niveau production à une API Express
- Définir des règles d'alerte qui détectent les vrais problèmes sans bruit
- Construire des tableaux de bord qui répondent vite aux questions d'astreinte
- Pratiquer un flux de réponse aux incidents qui réduit le temps moyen de résolution
C'est du concret. Vous obtiendrez du code, des règles et un petit pilote local à exécuter avant de toucher à la production.
Vue d'ensemble du workflow
Une approche claire par étapes réduit le retravail et aide les équipes à passer des idées à des implémentations fonctionnelles.
- Instrumenter
- Ajouter métriques et logs dans le processus de l'API
- Exposer un endpoint
/metricset des sondes de santé
- Collecter
- Scraper ou pousser les métriques vers votre base de séries temporelles
- Envoyer les logs structurés vers un magasin interrogeable
- Visualiser
- Construire des tableaux de bord pour les signaux dorés et les routes les plus lentes
- Alerter
- Définir des règles à fort signal liées à l'impact utilisateur et aux SLO
- Répondre
- Trier, atténuer et communiquer avec un runbook court
- Améliorer
- Ajuster les seuils, ajouter les labels manquants, corriger les alertes bruyantes
Métriques et signaux clés
Express se trouve à la frontière HTTP. Concentrez-vous d'abord sur les signaux dorés : latence, trafic, erreurs et saturation.
Métriques à collecter
- Requêtes totales : compteur étiqueté par méthode, route et status_code
- Erreurs totales : compteur, filtrer status_code >= 500
- Durée des requêtes : histogramme en millisecondes, étiqueté par méthode, route, status_code
- Requêtes actives : jauge pour détecter la saturation lors des pics
- Métriques de processus : latence de la boucle d'événements, tas utilisé, CPU, handles ouverts
- Latence/erreurs des dépendances : appels HTTP sortants, DB (ex. MongoDB), cache
Exemple d'instrumentation Express
Ajoutez des métriques légères avec prom-client. Les labels doivent être de faible cardinalité. Préférez les modèles de route (ex. /users/:id) aux chemins bruts.
// metrics.js
const client = require("prom-client");
// Métriques de processus Node.js par défaut
client.collectDefaultMetrics({ prefix: "node_" });
const httpRequestDurationMs = new client.Histogram({
name: "http_request_duration_ms",
help: "Durée des requêtes HTTP en ms",
labelNames: ["method", "route", "status_code"],
buckets: [10, 50, 100, 300, 500, 1000, 2000, 5000]
});
const httpRequestsTotal = new client.Counter({
name: "http_requests_total",
help: "Total des requêtes HTTP",
labelNames: ["method", "route", "status_code" ]
});
const httpRequestsErrorsTotal = new client.Counter({
name: "http_requests_errors_total",
help: "Total des réponses HTTP 5xx",
labelNames: ["method", "route" ]
});
const activeRequests = new client.Gauge({
name: "http_active_requests",
help: "Requêtes HTTP en cours",
labelNames: ["route" ]
});
function metricsMiddleware(req, res, next) {
const route = req.route && req.route.path ? req.route.path : req.path || "unknown\
activeRequests.labels(route).inc();
const end = httpRequestDurationMs.startTimer();
res.on("finish", () => {
const status = res.statusCode;
const labels = { method: req.method, route, status_code: String(status) };
httpRequestsTotal.labels(labels.method, labels.route, labels.status_code).inc();
if (status >= 500) {
httpRequestsErrorsTotal.labels(labels.method, labels.route).inc();
}
end(labels);
activeRequests.labels(route).dec();
});
next();
}
async function metricsEndpoint(req, res) {
res.set("Content-Type", client.register.contentType);
res.end(await client.register.metrics());
}
module.exports = {
metricsMiddleware,
metricsEndpoint
};
Intégration avec Express :
// server.js
const express = require("express");
const { metricsMiddleware, metricsEndpoint } = require("metrics");
const pino = require("pino");
const pinoHttp = require("pino-http");
const app = express();
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
app.use(pinoHttp({ logger, genReqId: () => crypto.randomUUID() }));
app.use(express.json());
app.use(metricsMiddleware);
app.get("healthz", (req, res) => res.status(200).send("ok\);
app.get("readyz", (req, res) => res.status(200).send("ready\);
// Routes d'exemple
app.get("users/:id", async (req, res) => {
// Simuler du travail
await new Promise(r => setTimeout(r, Math.random() * 200));
res.json({ id: req.params.id });
});
app.get("error", (req, res) => {
res.status(500).json({ error: "boom" });
});
app.get("metrics", metricsEndpoint);
const port = process.env.PORT || 3000;
app.listen(port, () => logger.info({ port }, "api-listening\);
Astuce : Assurez-vous que les labels de route utilisent des modèles (ex. /users/:id). La plupart des routeurs exposent req.route.path après la correspondance. Si le vôtre ne le fait pas, ajoutez une petite couche de mapping — retomber sur les chemins bruts cause une cardinalité élevée qui casse Prometheus.
Règles d'alerte qui comptent
Les règles d'alerte doivent être liées à l'impact visible par l'utilisateur et aux conditions soutenues. Évitez les pics sur un seul échantillon. Utilisez une fenêtre de page courte (5–15 min) et tempérez avec des clauses for.
Voici des règles d'exemple en syntaxe Prometheus. Adaptez les noms de métriques à votre schéma et stack.
Taux d'erreur
Pager quand le ratio 5xx dépasse 2 % pendant 5 minutes.
- alert: HighErrorRate
expr: (
sum(rate(http_requests_errors_total[5m]))
/
sum(rate(http_requests_total[5m]))
) > 0.02
for: 5m
labels:
severity: page
annotations:
summary: "Taux d'erreur 5xx élevé (>2% pendant 5m)"\n description: "Investiguer les déploiements récents, dépendances et logs."
Latence élevée (p95)
Pager quand la latence p95 dépasse 300 ms pendant 10 minutes.
- alert: HighLatencyP95
expr: |
histogram_quantile(0.95,
sum(rate(http_request_duration_ms_bucket[5m])) by (le)
) > 0.3
for: 10m
labels:
severity: page
annotations:
summary: "Latence p95 > 300ms pendant 10m"\n description: "Vérifier workers saturés, DB et routes chaudes."
Note : Prometheus stocke les durées en secondes. 0.3 == 300 ms.
Saturation
Pager quand les requêtes en vol sont constamment élevées par rapport à votre limite de concurrence.
- alert: SaturatedWorkers
expr: avg_over_time(http_active_requests[5m]) > 100
for: 10m
labels:
severity: page
annotations:
summary: "Nombre élevé de requêtes HTTP en vol"\n description: "Monter en charge ou écrêter la charge si nécessaire."
Ajustez le seuil à votre capacité de concurrence réelle (ex. nombre de workers × connexions max par worker).
Pas de trafic ou chute brutale
Avertir quand le taux de requêtes total tombe près de zéro pendant les heures de trafic attendu.
- alert: NoTraffic
expr: sum(rate(http_requests_total[5m])) < 1
for: 10m
labels:
severity: warn
annotations:
summary: "Taux de requêtes près de zéro"\n description: "Problème amont possible, DNS ou route LB."
Logs et traces
Quand une alerte se déclenche, vous avez besoin de réponses rapides. Structurez les logs pour la recherche et la corrélation.
Champs de log recommandés
ts: timestamp ISOlevel: info, warn, errormsg: message courtrequest_id: UUID généré par requêtemethod,route,pathstatus,latency_msuser_idouaccount_id(si disponible et sûr)remote_ip,user_agent(surveiller les politiques PII)service,version,commit_sha
Exemple Pino avec corrélation
const pino = require("pino");
const pinoHttp = require("pino-http");
const { randomUUID } = require("crypto");
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
app.use(pinoHttp({
logger,
genReqId: (req) => req.headers["x-request-id\] || randomUUID(),
customLogLevel: function (res, err) {
if (res.statusCode >= 500 || err) return "error"
if (res.statusCode >= 400) return "warn"
return "info"
},
customSuccessMessage: function (req, res) {
return "request-complete"
},
customErrorMessage: function (req, res, err) {
return "request-error"
},
serializers: {
req(req) {
return {
id: req.id,
method: req.method,
route: req.route && req.route.path ? req.route.path : req.url,
};
},
res(res) {
return { statusCode: res.statusCode };
},
},
}));
Signaux de log utiles
- Pics de 5xx et équivalents 499/ClientClosedRequest
- Timeouts (
ETIMEDOUT), resets (ECONNRESET), épuisement de pool - Erreurs répétées liées à une route, un tenant ou une version
- Queues longues : quelques requêtes prenant des secondes alors que la médiane est basse
Si vous adoptez le tracing distribué plus tard, commencez par propager request_id comme en-têtes trace-id à travers votre stack.
Tableaux de bord qui fonctionnent
Les tableaux de bord doivent répondre aux 5 questions principales d'astreinte en 1 minute.
Panneaux recommandés :
- Trafic : requêtes par seconde, par route et méthode
- Latence : tendances p50, p95, p99 ; top N routes lentes maintenant
- Erreurs : taux d'erreur par status_code et route
- Saturation : requêtes en vol, latence boucle d'événements, CPU, tas utilisé, pauses GC
- Dépendances : latence et erreurs DB, HTTP sortant
- Répartition par version : métriques splitées par version/commit pour décisions de rollback rapides
Astuces de mise en page :
- Garder une vue de triage mono-page avec dashboards de drill-down liés par service
- Annoter avec marqueurs de déploiement
- Utiliser couleurs et unités cohérentes ; millisecondes pour la latence est lisible d'un coup d'œil
Workflow de réponse aux incidents
Un flux léger et reproductible transforme les alertes en récupération rapide.
- Acquitter et évaluer
- Confirmer l'alerte et vérifier le dashboard de triage
- Identifier l'impact utilisateur (taux d'erreur, latence, chute de trafic)
- Stabiliser
- Revenir sur les changements récents si les métriques pointent vers une régression
- Monter en charge, ajouter un circuit breaker, ou rate-limiter les tenants bruyants
- Diagnostiquer
- Filtrer les logs par
request_id, route et version - Comparer les panneaux routes chaudes et dépendances
- Atténuer
- Déployer un fix config, feature flag, ou hot patch
- Ajouter timeouts ou retries temporaires pour dépendances instables
- Vérifier
- S'assurer que les alertes se résolvent et les signaux dorés reviennent au baseline
- Apprendre
- Capturer ce qui a détecté le problème, ce qui a aidé, ce qui manquait
- Ajuster alertes et dashboards. Ajouter labels ou nouvelles métriques si nécessaire
Garder un runbook court par alerte avec responsable, vérifications et remèdes.
Plan de pilote local
Commencez petit, mesurez et inspectez localement avant le déploiement. Un pilote étroit et mesurable réduit le risque et clarifie les prochaines étapes.
1) Lancer l'API localement
Installer les dépendances :
npm i express prom-client pino pino-http
node server.js
Tester les endpoints et vérifier les métriques :
curl -s localhost:3000/healthz
curl -s localhost:3000/users/123
curl -s localhost:3000/error
curl -s localhost:3000/metrics | head
2) Optionnel : scraper métriques et alerter localement
Utiliser un docker-compose minimal pour lancer un magasin de métriques, un dashboard et un composant d'alerting.
version: "3.9"
services:
api:
build: .
ports: ["3000:3000\)
prometheus:
image: prom/prometheus:latest
ports: ["9090:9090\)
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
alertmanager:
image: prom/alertmanager:latest
ports: ["9093:9093\)
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
grafana:
image: grafana/grafana:latest
ports: ["3001:3000\)
Exemple prometheus.yml :
global:
scrape_interval: 15s
scrape_configs:
- job_name: "express-api"
metrics_path: /metrics
static_configs:
- targets: ["api:3000\)
rule_files:
- rules.yml
Exemple rules.yml avec deux alertes :
groups:
- name: express
rules:
- alert: HighErrorRate
expr: (sum(rate(http_requests_errors_total[5m])) / sum(rate(http_requests_total[5m]))) > 0.02
for: 5m
labels: { severity: page }
annotations:
summary: "Taux d'erreur 5xx élevé"
- alert: HighLatencyP95
expr: histogram_quantile(0.95, sum(rate(http_request_duration_ms_bucket[5m])) by (le)) > 0.3
for: 10m
labels: { severity: page }
annotations:
summary: "Latence p95 > 300ms"
3) Générer du trafic de test
# trafic régulier sain
for i in $(seq 1 200); do curl -s localhost:3000/users/$i > /dev/null; done
# induire latence et erreurs
curl -s localhost:3000/error > /dev/null
Surveiller le dashboard et valider que les alertes se déclenchent seulement quand les conditions persistent.
4) Définir critères de réussite et documenter
- Réussi si métriques peuplées, dashboards rendus, alertes tirent et se résolvent comme attendu
- Échoué si labels explosent en cardinalité, alertes bruyantes, ou métriques manquantes
- Capturer seuils, sets de labels, et gaps à corriger avant déploiement en staging
Conclusion
Vous avez maintenant une base pratique pour le monitoring d'API Express :
- Métriques signaux dorés et labels de faible cardinalité
- Règles d'alerte actionnables reflétant l'impact utilisateur
- Logs corrélant requêtes et accélérant le diagnostic
- Tableaux de bord répondant aux questions d'astreinte vite
- Flux de réponse aux incidents simple et pilote local sûr
Prochaines étapes :
- Ajuster les seuils à votre trafic et capacité réels
- Ajouter métriques de dépendances pour bases de données et appels sortants
- Étendre les logs avec propagation
request_idacross services - Piloter sur un seul service en staging, puis déployer progressivement
Un petit départ mesurable et un workflow clair aident les équipes à passer des idées à une API Express fiable et observable.