From a850e5dd2839acbd1db142d290154897531e33b9 Mon Sep 17 00:00:00 2001 From: soufiane Date: Wed, 26 Nov 2025 10:54:45 +0100 Subject: [PATCH] feat: add HTTP metrics middleware for Prometheus monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add custom metrics: http_requests_total, http_request_duration_seconds, http_errors_total, http_requests_in_progress, http_response_size_bytes - Track method, route, and status_code labels - Normalize routes to avoid high cardinality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- index.js | 4 ++ src/middleware/metrics.js | 108 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/middleware/metrics.js diff --git a/index.js b/index.js index 0de5f80f..89c38137 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ import client from "prom-client"; import config from "./src/config/env.js"; import { pool } from "./db.js"; import { errorHandler } from "./src/middleware/errorHandler.js"; +import { metricsMiddleware } from "./src/middleware/metrics.js"; // Import routes import authRoutes from "./src/routes/auth.routes.js"; @@ -58,6 +59,9 @@ app.use(helmet({ app.use(morgan("tiny")); app.use(express.json()); +// Middleware de métriques HTTP (doit être avant les routes) +app.use(metricsMiddleware); + // Servir les fichiers statiques depuis le dossier public app.use('/public', express.static('public')); diff --git a/src/middleware/metrics.js b/src/middleware/metrics.js new file mode 100644 index 00000000..f909f541 --- /dev/null +++ b/src/middleware/metrics.js @@ -0,0 +1,108 @@ +import client from "prom-client"; + +// Compteur de requêtes HTTP totales +const httpRequestsTotal = new client.Counter({ + name: "http_requests_total", + help: "Total number of HTTP requests", + labelNames: ["method", "route", "status_code"], +}); + +// Histogramme de durée des requêtes +const httpRequestDuration = new client.Histogram({ + name: "http_request_duration_seconds", + help: "Duration of HTTP requests in seconds", + labelNames: ["method", "route", "status_code"], + buckets: [0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1, 2, 5], +}); + +// Compteur de requêtes en cours +const httpRequestsInProgress = new client.Gauge({ + name: "http_requests_in_progress", + help: "Number of HTTP requests currently being processed", + labelNames: ["method"], +}); + +// Compteur d'erreurs HTTP +const httpErrorsTotal = new client.Counter({ + name: "http_errors_total", + help: "Total number of HTTP errors (4xx and 5xx)", + labelNames: ["method", "route", "status_code"], +}); + +// Taille des réponses +const httpResponseSize = new client.Histogram({ + name: "http_response_size_bytes", + help: "Size of HTTP responses in bytes", + labelNames: ["method", "route"], + buckets: [100, 500, 1000, 5000, 10000, 50000, 100000, 500000], +}); + +// Fonction pour normaliser les routes (éviter la cardinalité élevée) +const normalizeRoute = (req) => { + // Si la route est définie par Express, l'utiliser + if (req.route && req.route.path) { + return req.baseUrl + req.route.path; + } + + // Sinon, normaliser le path en remplaçant les IDs par des placeholders + let path = req.path; + + // Remplacer les UUIDs + path = path.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ":id"); + + // Remplacer les nombres (IDs numériques) + path = path.replace(/\/\d+/g, "/:id"); + + // Remplacer les codes de ticket (format spécifique) + path = path.replace(/\/[A-Z0-9]{6,}/g, "/:code"); + + return path; +}; + +// Middleware de métriques +export const metricsMiddleware = (req, res, next) => { + // Ignorer les endpoints de métriques et health check + if (req.path === "/metrics" || req.path === "/health") { + return next(); + } + + const startTime = Date.now(); + + // Incrémenter les requêtes en cours + httpRequestsInProgress.inc({ method: req.method }); + + // Capturer la fin de la réponse + res.on("finish", () => { + const duration = (Date.now() - startTime) / 1000; // En secondes + const route = normalizeRoute(req); + const statusCode = res.statusCode.toString(); + const labels = { + method: req.method, + route: route, + status_code: statusCode, + }; + + // Enregistrer les métriques + httpRequestsTotal.inc(labels); + httpRequestDuration.observe(labels, duration); + httpRequestsInProgress.dec({ method: req.method }); + + // Compter les erreurs + if (res.statusCode >= 400) { + httpErrorsTotal.inc(labels); + } + + // Taille de la réponse (si disponible) + const contentLength = res.get("Content-Length"); + if (contentLength) { + httpResponseSize.observe( + { method: req.method, route: route }, + parseInt(contentLength, 10) + ); + } + }); + + next(); +}; + +export default metricsMiddleware;