feat: add HTTP metrics middleware for Prometheus monitoring

- 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 <noreply@anthropic.com>
This commit is contained in:
soufiane 2025-11-26 10:54:45 +01:00
parent 7d295e6883
commit a850e5dd28
2 changed files with 112 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import client from "prom-client";
import config from "./src/config/env.js"; import config from "./src/config/env.js";
import { pool } from "./db.js"; import { pool } from "./db.js";
import { errorHandler } from "./src/middleware/errorHandler.js"; import { errorHandler } from "./src/middleware/errorHandler.js";
import { metricsMiddleware } from "./src/middleware/metrics.js";
// Import routes // Import routes
import authRoutes from "./src/routes/auth.routes.js"; import authRoutes from "./src/routes/auth.routes.js";
@ -58,6 +59,9 @@ app.use(helmet({
app.use(morgan("tiny")); app.use(morgan("tiny"));
app.use(express.json()); 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 // Servir les fichiers statiques depuis le dossier public
app.use('/public', express.static('public')); app.use('/public', express.static('public'));

108
src/middleware/metrics.js Normal file
View File

@ -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;