Cours NestJS · Module 6: Gestion des erreurs et monitoring
Monitoring et Alerting - les systèmes d'alerte précoce
Dans cette leçon6
L'application fonctionne. Aucune exception n'a été levée, les journaux se taisent, le health check est au vert. Et pourtant les utilisateurs écrivent que « le site rame ». Tu vérifies - les réponses arrivent au bout de huit cents millisecondes au lieu de cent. Depuis quand ? Nul ne le sait. Les journaux consignent des événements, et ceci est une tendance : quelque chose qui a grandi pendant des semaines et qu'aucune entrée isolée ne montre.
Rome dressait des tours à signaux sur ses frontières. Elles ne rapportaient pas des événements isolés - elles mesuraient le trafic : combien de cavaliers sont passés, combien de temps dure la traversée, combien de sentinelles ne sont pas revenues. C'est seulement d'après ces nombres qu'on voyait quelque chose se dégrader avant qu'une porte ne tombe. Voilà ce que sont les métriques.
Quatre types de métriques
Le standard de collecte des métriques est Prometheus - un système qui collecte et stocke les nombres décrivant ton application. Il propose quatre sortes de mesure, de la plus simple à la plus élaborée :
- Counter - ne fait que monter. Requêtes servies, erreurs rencontrées. Il ne décroît jamais ; au redémarrage il repart de zéro.
- Gauge - peut monter et descendre. Connexions actives, mémoire utilisée, longueur de file. Il relève la valeur courante, comme l'aiguille d'un cadran.
- Histogram - la répartition des valeurs dans des seaux (buckets). Au lieu d'un seul nombre, il retient combien de mesures sont tombées dans 0-100 ms, combien dans 100-500 ms, et ainsi de suite.
- Summary - comme un histogramme, mais avec des percentiles calculés côté client, c'est-à-dire dans ton application et non dans Prometheus.
Pour la durée des requêtes HTTP, le bon choix est l'Histogram. Un Counter dirait seulement combien il y a eu de requêtes, un Gauge combien a duré la dernière. Un Histogram montre la forme : que neuf requêtes sur dix s'achèvent sous 200 ms tandis qu'une sur dix dépasse la seconde. C'est cette forme qui révèle un problème qu'une moyenne masquerait.
Définir une métrique
Tu crées une métrique une fois, en la décrivant par trois champs :
1import { Counter } from 'prom-client';
2
3@Injectable()
4export class PrometheusService {
5 private readonly httpRequestCounter = new Counter({
6 name: 'http_requests_total',
7 help: 'Total HTTP requests',
8 labelNames: ['method', 'route', 'status'],
9 });
10
11 recordRequest(method: string, route: string, status: number) {
12 this.httpRequestCounter.inc({ method, route, status: String(status) });
13 }
14}L'ordre des champs est conventionnel mais toujours le même : name est l'identifiant de la métrique, help une description lisible par un humain, labelNames la liste des dimensions selon lesquelles tu pourras découper les données.
Les étiquettes sont la partie intéressante. Grâce à elles, un seul compteur répond à plusieurs questions : combien de requêtes POST, combien ont visé /legions, combien se sont soldées par un 500. Sans étiquettes, il te faudrait un compteur distinct pour chaque combinaison.
Attention à un piège : une étiquette aux valeurs nombreuses multiplie le nombre de séries de données. Mettre un identifiant d'utilisateur en étiquette crée autant de séries que tu as d'utilisateurs - et mettra Prometheus à terre. Les étiquettes doivent avoir un ensemble de valeurs fini et petit.
Des métriques système gratuites
Avant d'écrire tes propres métriques, il vaut la peine d'activer celles qui sont intégrées :
1import { collectDefaultMetrics, register } from 'prom-client';
2
3collectDefaultMetrics();collectDefaultMetrics() collecte les métriques système par défaut - usage du processeur, de la mémoire et latence de la boucle d'événements de Node.js. Cette dernière est particulièrement précieuse : une latence croissante de l'event loop signifie que quelque chose bloque le thread et que l'application décroche, alors qu'aucun endpoint n'est encore tombé.
Note ce que cette fonction ne fait pas : elle n'envoie rien, ne trace aucun graphique et ne remet aucun compteur à zéro. Elle ne fait que commencer à collecter.
L'endpoint /metrics
Prometheus n'accepte pas les données poussées par une application - il vient les chercher lui-même. Ton rôle est de les exposer à une adresse convenue :
1@Controller()
2export class MetricsController {
3 @Get('/metrics')
4 async getMetrics(@Res() res: Response) {
5 res.set('Content-Type', register.contentType);
6 res.send(await register.metrics());
7 }
8}register est le registre de toutes les métriques définies, et register.metrics() les renvoie au format texte que Prometheus comprend. L'en-tête Content-Type doit indiquer du texte brut, non du JSON - d'où register.contentType, qui fixe la bonne valeur à ta place.
Ce modèle s'appelle le scraping : toutes les quinzaines de secondes, Prometheus interroge cette adresse et enregistre ce qu'il y trouve. L'application n'a pas besoin de savoir qui l'observe, ni si quelqu'un l'observe.
Cinq étapes de mise en place
Toute la route, de rien jusqu'au graphique, ressemble à ceci :
- Installation du paquet
prom-client. - Définition des métriques - Counter, Histogram, autant qu'il en faut.
- Création de l'endpoint
/metrics. - Configuration du scraper Prometheus, pour qu'il sache où et à quelle fréquence demander.
- Visualisation des métriques dans Grafana - et c'est seulement ici qu'apparaissent graphiques et alertes.
Le partage des rôles entre les deux derniers se brouille souvent. Prometheus collecte et stocke, Grafana trace et alerte. Cette séparation te permet de remplacer l'un sans l'autre - et fait que l'application ne connaît ni l'un ni l'autre.
Récapitulatif
Les tours à signaux sont dressées, le trafic est mesuré :
- les journaux consignent des événements, les métriques montrent des tendances - la seconde est invisible dans toute entrée isolée,
- Prometheus collecte et stocke des métriques ; il ne journalise pas les erreurs et ne répare rien,
- quatre types du plus simple : Counter (ne fait que monter), Gauge (monte et descend), Histogram (répartition en seaux), Summary (percentiles calculés côté client),
- pour la durée des requêtes HTTP, le bon type est l'Histogram - il montre la forme qu'une moyenne masquerait,
- trois champs définissent une métrique :
name,help,labelNames, - les étiquettes permettent de découper les données mais doivent avoir un petit ensemble de valeurs - un identifiant d'utilisateur en étiquette mettra Prometheus à terre,
collectDefaultMetrics()collecte les métriques système - CPU, mémoire, latence de l'event loop ; elle n'envoie rien,- Prometheus vient chercher les données lui-même (scraping), tu exposes donc un endpoint
/metricsavecContent-Typeréglé sur du texte brut viaregister.contentType, - cinq étapes : installer
prom-client, définir les métriques, l'endpoint/metrics, configurer le scraper, visualiser dans Grafana, - Prometheus collecte, Grafana trace et alerte.
Dans la prochaine leçon, nous descendrons des tendances jusqu'à un bug isolé - tu découvriras les techniques de débogage pour le moment où l'on sait que quelque chose cloche, mais pas où. En attendant, retiens ceci : un journal dit ce qui est arrivé une fois ; une métrique dit ce qui arrive sans cesse - et c'est elle qui prévient avant que la porte ne tombe.
Code de cette leçon : src/monitoring/monitoring-system.ts
1// Monitoring i Alerting - Systemy Wczesnego Ostrzegania
2// Métriques, alertes et tableau de bord pour l'Imperium
3import { Injectable, Logger } from '@nestjs/common';
4
5// ===========================================
6// 1. Service de métriques (style Prometheus)
7// ===========================================
8
9@Injectable()
10export class MetricsService {
11 private logger = new Logger('Metrics');
12
13 // Counters - ne font qu'augmenter
14 private counters = new Map<string, number>();
15
16 // Histograms - distribution des valeurs
17 private histograms = new Map<string, number[]>();
18
19 // Gauge - valeur instantanée (peut augmenter et diminuer)
20 private gauges = new Map<string, number>();
21
22 // Incrémenter le compteur
23 incrementCounter(name: string, value: number = 1) {
24 const current = this.counters.get(name) || 0;
25 this.counters.set(name, current + value);
26 }
27
28 // Enregistrer une valeur dans l'histogramme
29 observeHistogram(name: string, value: number) {
30 if (!this.histograms.has(name)) {
31 this.histograms.set(name, []);
32 }
33 this.histograms.get(name).push(value);
34 }
35
36 // Définir la gauge
37 setGauge(name: string, value: number) {
38 this.gauges.set(name, value);
39 }
40
41 // Récupérer toutes les métriques
42 getMetrics() {
43 const result: Record<string, any> = {};
44
45 // Counters
46 this.counters.forEach((v, k) => {
47 result[k + '_total'] = v;
48 });
49
50 // Histograms - moyenne et percentiles
51 this.histograms.forEach((values, k) => {
52 const sorted = [...values].sort((a, b) => a - b);
53 const sum = values.reduce((a, b) => a + b, 0);
54 result[k + '_avg'] = Math.round(sum / values.length);
55 result[k + '_p95'] = sorted[Math.floor(sorted.length * 0.95)];
56 result[k + '_count'] = values.length;
57 });
58
59 // Gauges
60 this.gauges.forEach((v, k) => {
61 result[k] = v;
62 });
63
64 return result;
65 }
66}
67
68// ===========================================
69// 2. Service d'alertes
70// ===========================================
71
72@Injectable()
73export class AlertService {
74 private logger = new Logger('Alerting');
75 private alerts: Array<{
76 name: string;
77 severity: string;
78 message: string;
79 timestamp: Date;
80 }> = [];
81
82 // Définitions des seuils d'alerte
83 private thresholds = {
84 error_rate: { warn: 0.01, critical: 0.05 },
85 response_time_ms: { warn: 500, critical: 2000 },
86 memory_percent: { warn: 80, critical: 95 },
87 };
88
89 checkThreshold(metric: string, value: number) {
90 const threshold = this.thresholds[metric];
91 if (!threshold) return;
92
93 if (value >= threshold.critical) {
94 this.fireAlert(metric, 'CRITICAL', metric + ' = ' + value);
95 } else if (value >= threshold.warn) {
96 this.fireAlert(metric, 'WARNING', metric + ' = ' + value);
97 }
98 }
99
100 private fireAlert(name: string, severity: string, message: string) {
101 const alert = { name, severity, message, timestamp: new Date() };
102 this.alerts.push(alert);
103 this.logger.warn('ALERT [' + severity + ']: ' + message);
104 }
105
106 getActiveAlerts() {
107 return this.alerts.slice(-20);
108 }
109}
110
111console.log('=== Monitoring & Alerting ===');
112console.log('Counter - compte les événements (requêtes, erreurs)');
113console.log('Histogram - distribution des temps de réponse');
114console.log('Gauge - valeur actuelle (mémoire, connexions)');
115console.log("Seuils d'alerte : warn -> critical");
116Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. À quoi sert Prometheus dans le contexte de la surveillance des applications NestJS ?
2. Quel type de métrique Prometheus est le mieux adapté pour mesurer la durée des requêtes HTTP ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète le PrometheusService avec un champ httpRequestCounter (Counter avec labelNames : ['method', 'route', 'status_code']) et httpRequestDuration (Histogram avec buckets : [0.1, 0.5, 1, 2, 5, 10])
- Tri vertical
Ordonne les types de métriques Prometheus du plus simple au plus complexe
- Clique dans l'ordre
Ordonne correctement les éléments de la définition d'un Counter avec labelNames
- Éditeur de code
Complète le MetricsController avec @Get('/metrics') qui définit le Content-Type à 'text/plain' et renvoie register.metrics() de prom-client
- Tri vertical
Ordonne les étapes d'implémentation du monitoring Prometheus dans une application NestJS, de l'installation à la visualisation