Cours NestJS · Module 12: Conteneurs et CI/CD

Observability - les métriques avec Prometheus

5 min de lecture
Dans cette leçon5

Tu as rencontré Prometheus dans le module sur le monitoring : quatre types de métriques, le scraping, et un endpoint /metrics écrit à la main dans un contrôleur. Cela fonctionnait - mais chaque nouvelle métrique imposait de penser au registre, et chaque module d'injecter le service qui les détient.

NestJS a une réponse toute prête. @willsoto/nestjs-prometheus collecte et expose les métriques de l'application au format Prometheus - et au passage transforme les métriques en providers ordinaires, injectés comme n'importe quoi d'autre.

Enregistrer les métriques

Tu déclares les métriques dans un module, par les fonctions utilitaires du paquet :

1@Module({
2  imports: [PrometheusModule.register()],
3  providers: [
4    makeCounterProvider({
5      name: 'http_requests_total',
6      help: 'Nombre total de requetes HTTP',
7      labelNames: ['method', 'route', 'status'],
8    }),
9    makeGaugeProvider({
10      name: 'active_connections',
11      help: 'Nombre de connexions actives',
12    }),
13    makeHistogramProvider({
14      name: 'http_request_duration_seconds',
15      help: 'Duree de traitement des requetes HTTP',
16      labelNames: ['method', 'route'],
17    }),
18  ],
19})
20export class MetricsModule {}

L'ordre à l'intérieur de makeCounterProvider est fixe : makeCounterProvider({, puis name: 'http_requests_total',, ensuite help: '...',, et enfin }).

PrometheusModule.register() expose l'endpoint /metrics - tu ne l'écris plus toi-même. Les trois fonctions utilitaires correspondent aux trois types de métriques que tu connais : un Counter ne fait que monter (nombre de requêtes), un Gauge monte et descend (connexions actives), un Histogram mesure une répartition en seaux (temps de réponse). Le quatrième type, Summary, calcule des quantiles dans l'application.

L'usage dans un service

Tu injectes une métrique enregistrée comme n'importe quel provider - sauf que tu la désignes par son nom :

1@Injectable()
2export class MetricsService {
3  constructor(
4    @InjectMetric('http_requests_total')
5    private readonly requestsCounter: Counter<string>,
6
7    @InjectMetric('http_request_duration_seconds')
8    private readonly requestDuration: Histogram<string>,
9  ) {}
10
11  recordRequest(method: string, route: string, status: number) {
12    this.requestsCounter.inc({ method, route, status: String(status) });
13  }
14
15  startTimer(method: string, route: string) {
16    return this.requestDuration.startTimer({ method, route });
17  }
18}

@InjectMetric('nom') fonctionne comme l'@Inject à token rencontré avec les providers useValue - une métrique n'est pas une classe, NestJS ne peut donc pas la choisir par son type.

Deux méthodes méritent d'être distinguées. inc() augmente le compteur de un ; c'est tout ce qu'un Counter sait faire. startTimer() renvoie une fonction que tu appelles une fois le travail terminé - c'est seulement alors que l'histogramme enregistre la durée mesurée :

1const timer = this.metricsService.startTimer('GET', '/legions');
2
3await this.legionService.findAll();
4
5timer();

Ce motif - prends une fonction, fais le travail, appelle la fonction - est plus commode que de soustraire des horodatages à la main, et il gère l'unité : les histogrammes Prometheus comptent en secondes, non en millisecondes.

La méthode RED - que mesurer en premier

On pourrait ajouter des centaines de métriques. RED est l'acronyme qui nomme les trois par lesquelles on commence toujours, classées par importance :

  1. Rate - le nombre de requêtes par seconde. Il dit s'il y a du trafic.
  2. Errors - la part des requêtes en échec. Elle dit si le trafic est servi.
  3. Duration - le temps de réponse, d'ordinaire en p95 et p99. Il dit s'il est bien servi.

L'ordre n'est pas arbitraire et mérite d'être retenu : un Rate tombé à zéro signifie que personne ne peut se connecter - alarme immédiate. Des Errors en hausse sont une panne visible des utilisateurs. Une Duration en hausse est un problème encore en formation.

Note pourquoi le temps se donne en percentiles plutôt qu'en moyenne. Une moyenne de 200 ms sonne bien même quand un utilisateur sur vingt attend cinq secondes - p95 dit franchement : « 95% des requêtes sont passées sous cette valeur ». C'est un nombre qui décrit l'expérience au lieu de la brouiller.

Ce que renvoie /metrics

L'endpoint exposé par le paquet restitue les données au format texte de Prometheus - ni JSON, ni XML, ni CSV :

1# HELP http_requests_total Nombre total de requetes HTTP
2# TYPE http_requests_total counter
3http_requests_total{method="GET",route="/legions",status="200"} 1027
4http_requests_total{method="POST",route="/legions",status="201"} 43

La forme est simple et mérite qu'on sache la lire. Les lignes commençant par # HELP portent la description de la métrique, celles par # TYPE son type. Viennent ensuite les mesures elles-mêmes : le nom, les étiquettes entre accolades, la valeur.

Ce format paraît pauvre à côté du JSON, mais c'est bien l'intention : Prometheus interroge des milliers d'applications toutes les quinzaines de secondes, l'analyse doit donc coûter peu. Quand tu ouvres /metrics dans un navigateur et vois un mur de texte - c'est que cela fonctionne.

Récapitulatif

Les yeux de l'Empire veillent, et les métriques sont des providers ordinaires :

  • @willsoto/nestjs-prometheus collecte et expose les métriques au format Prometheus - il ne gère pas les WebSockets, ne teste pas les endpoints et ne génère pas de documentation,
  • PrometheusModule.register() expose l'endpoint /metrics à ta place,
  • tu enregistres les métriques comme providers : makeCounterProvider, makeGaugeProvider, makeHistogramProvider,
  • ordre d'enregistrement : makeCounterProvider({, name:, help:, }),
  • un Counter ne peut que monter (nombre de requêtes) ; un Gauge monte et descend ; un Histogram mesure une répartition en seaux ; un Summary calcule des quantiles,
  • @InjectMetric('nom') injecte une métrique par son nom, car ce n'est pas une classe et le type ne suffit pas,
  • inc() augmente le compteur ; startTimer() renvoie une fonction à appeler après le travail - les histogrammes comptent en secondes,
  • la méthode RED par ordre d'importance : Rate (requêtes par seconde) → Errors (part d'échecs) → Duration (p95, p99),
  • donne le temps en percentiles, non en moyenne - une moyenne cache l'utilisateur sur vingt qui attend cinq secondes,
  • /metrics renvoie le format texte de Prometheus : # HELP, # TYPE, puis le nom avec ses étiquettes et une valeur - ni JSON, ni XML, ni CSV.

Dans la prochaine leçon, nous irons au-delà des nombres - tu découvriras le distributed tracing, qui montre la route d'une seule requête à travers plusieurs services. En attendant, retiens ceci : les métriques disent combien et à quelle vitesse ; RED nomme les trois nombres à vérifier en premier.

Code de cette leçon : src/prometheus-metrics.ts
1// Observabilité - Métriques avec Prometheus
2console.log("=== PROMETHEUS METRICS ===\n");
3
4// Types de métriques Prometheus
5interface MetricType {
6  name: string;
7  type: 'counter' | 'gauge' | 'histogram' | 'summary';
8  description: string;
9  example: string;
10}
11
12const metricTypes: MetricType[] = [
13  {
14    name: 'Counter',
15    type: 'counter',
16    description: 'Augmente uniquement (ex. nombre de requêtes)',
17    example: 'http_requests_total{method="GET",status="200"} 1547',
18  },
19  {
20    name: 'Gauge',
21    type: 'gauge',
22    description: 'Augmente et diminue (ex. connexions actives)',
23    example: 'active_connections 42',
24  },
25  {
26    name: 'Histogram',
27    type: 'histogram',
28    description: 'Distribution des valeurs par intervalles (ex. temps de réponse)',
29    example: 'http_request_duration_seconds_bucket{le="0.1"} 1400',
30  },
31  {
32    name: 'Summary',
33    type: 'summary',
34    description: 'Quantiles (p50, p95, p99)',
35    example: 'request_duration_summary{quantile="0.95"} 0.25',
36  },
37];
38
39console.log("Types de métriques Prometheus :\n");
40metricTypes.forEach((m, i) => {
41  console.log(`${i + 1}. ${m.name} (${m.type})`);
42  console.log(`   Description : ${m.description}`);
43  console.log(`   Exemple : ${m.example}\n`);
44});
45
46// Méthode RED
47console.log("=== MÉTHODE RED ===\n");
48const red = {
49  Rate: 'Nombre de requêtes par seconde',
50  Errors: 'Pourcentage de requêtes en erreur',
51  Duration: 'Temps de réponse (p95, p99)',
52};
53
54Object.entries(red).forEach(([k, v]) => {
55  console.log(`  ${k} : ${v}`);
56});
57
58console.log("\n=== ENDPOINT /metrics ===\n");
59console.log("# HELP http_requests_total Total number of requests");
60console.log("# TYPE http_requests_total counter");
61console.log('http_requests_total{method="GET",status="200"} 1547');
62

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 le package @willsoto/nestjs-prometheus ?

  2. 2. Qu'est-ce qui caractérise un type de métrique Counter dans Prometheus ?

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

    Définissez trois métriques : http_requests_total (Counter avec labelNames : method, route, status), active_connections (Gauge), http_request_duration (Histogram)

  • Tri vertical

    Ordonnez les éléments de la méthode RED du plus important au moins fréquemment vérifié :

  • Clique dans l'ordre

    Ordonnez les éléments de l'enregistrement d'une métrique Counter dans PrometheusModule :

  • Éditeur de code

    Injectez les métriques via @InjectMetric, utilisez requestsCounter.inc() et requestDuration.startTimer() / timer()