## 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 /metrics et 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.

</div>

### 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 ISO

- level : info, warn, error

- msg : message court

- request_id : UUID généré par requête

- method , route , path

- status , latency_ms

- user_id ou account_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_id across 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.