Cours NestJS · Module 6: Gestion des erreurs et monitoring

Monitoring et Alerting - les systèmes d'alerte précoce

6 min de lecture
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 :

  1. 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.
  2. 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.
  3. 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.
  4. 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 :

  1. Installation du paquet prom-client.
  2. Définition des métriques - Counter, Histogram, autant qu'il en faut.
  3. Création de l'endpoint /metrics.
  4. Configuration du scraper Prometheus, pour qu'il sache où et à quelle fréquence demander.
  5. 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 /metrics avec Content-Type réglé sur du texte brut via register.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");
116

Tu 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. 1. À quoi sert Prometheus dans le contexte de la surveillance des applications NestJS ?

  2. 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