Cours NestJS · Module 6: Gestion des erreurs et monitoring

Health Checks - le diagnostic de l'état

6 min de lecture
Dans cette leçon6

L'application a démarré, le processus tourne, le port répond. Mais est-elle vraiment apte au service ? La base a pu refuser une connexion, le disque peut être plein, une API externe peut se taire. Le processus vit, et chaque requête échouera quand même.

Un camp romain avait pour cela l'appel du matin. On ne comptait pas combien de légionnaires respiraient - on vérifiait tour à tour : le puits donne-t-il de l'eau, le magasin n'est-il pas vide, le messager du fort voisin est-il arrivé. Seule la somme de ces réponses disait si la cohorte pouvait marcher. Voilà ce qu'est un health check.

Le paquet et son cœur

Dans NestJS, c'est le paquet @nestjs/terminus qui s'en charge. Le nom n'aide pas - le mot « health » n'y figure pas - il est donc facile de chercher quelque chose comme @nestjs/health ou @nestjs/monitor. De tels paquets n'existent pas.

Son élément central est HealthCheckService, qui exécute un ensemble d'indicateurs et renvoie le statut agrégé de l'application. Il ne mesure rien lui-même et - point important - ne redémarre pas l'application et n'envoie pas d'alertes. Il rassemble les réponses et communique le résultat ; qu'en faire, c'est l'affaire de celui qui a demandé.

L'appel du matin

Un contrôleur de santé ressemble à ceci :

1@Controller('health')
2export class HealthController {
3  constructor(
4    private health: HealthCheckService,
5    private db: TypeOrmHealthIndicator,
6    private memory: MemoryHealthIndicator,
7  ) {}
8
9  @Get()
10  @HealthCheck()
11  check() {
12    return this.health.check([
13      () => this.db.pingCheck('database'),
14      () => this.memory.checkHeap('memory', 150 * 1024 * 1024),
15    ]);
16  }
17}

L'ordre de l'appel est toujours le même : this.health.check([ ouvre la liste, les indicateurs individuels se placent à l'intérieur, et ]) la referme.

Note que tu passes chaque indicateur comme une fonction, et non comme le résultat de son appel. Écrire () => this.db.pingCheck('database') permet au service de lancer lui-même les vérifications - en parallèle et avec gestion des erreurs. Si tu avais écrit this.db.pingCheck('database') sans la flèche, la vérification s'exécuterait immédiatement, hors du contrôle du service, et une exception levée emporterait tout l'endpoint.

Le premier argument de chaque indicateur est une clé - le nom sous lequel son résultat apparaîtra dans la réponse. C'est toi qui la choisis.

Les indicateurs intégrés

Terminus fournit plusieurs indicateurs prêts, un par type de ressource :

  • TypeOrmHealthIndicator - vérifie la connexion à la base de données par pingCheck.
  • HttpHealthIndicator - interroge une adresse HTTP externe pour voir si l'API d'autrui répond.
  • MemoryHealthIndicator - surveille l'usage mémoire ; checkHeap prend un seuil en octets.
  • DiskHealthIndicator - vérifie l'espace disque libre.

Les noms se ressemblent assez pour qu'il vaille la peine de les retenir par la ressource concernée : TypeOrm - la base, Http - une API externe, Memory - la mémoire, Disk - le disque.

La forme de la réponse

Le résultat de l'appel comporte trois niveaux de détail, du général au particulier :

1{
2  "status": "ok",
3  "info": { "database": { "status": "up" } },
4  "details": { "database": { "status": "up" }, "memory": { "status": "up" } }
5}

status est un seul mot pour l'ensemble - 'ok' quand tout est passé, ou 'error' dès que quelque chose a échoué. C'est lui que lit le système qui interroge la santé de l'application toutes les quinzaines de secondes.

info ne contient que les indicateurs en bon état, tandis que son champ jumeau error ne contient que ceux qui ont échoué. details est l'ensemble complet : tous les indicateurs, quel que soit le résultat.

Ce partage a un intérêt pratique : un humain regarde details pour avoir l'image entière, tandis qu'un automate lit status, car cette seule valeur décide de son action.

Un indicateur maison

Les indicateurs intégrés surveillent l'infrastructure. Quand tu veux vérifier quelque chose de ton domaine - si le trésor accepte les dépôts, si une légion est au complet - tu écris toi-même un indicateur :

1@Injectable()
2export class LegionHealthIndicator extends HealthIndicator {
3  constructor(private legionsService: LegionsService) {
4    super();
5  }
6
7  async isHealthy(key: string): Promise<HealthIndicatorResult> {
8    const count = await this.legionsService.countActive();
9
10    if (count > 0) {
11      return this.getStatus(key, true, { activeLegions: count });
12    }
13
14    throw new HealthCheckError(
15      'Aucune légion active',
16      this.getStatus(key, false, { activeLegions: 0 }),
17    );
18  }
19}

La classe hérite de HealthIndicator et implémente la méthode isHealthy(key). Le contrat a deux versants et mérite d'être retenu : en cas de succès tu renvoies this.getStatus(key, true), et en cas d'échec tu lèves HealthCheckError portant le même statut, seulement avec false.

Pourquoi une exception plutôt qu'un false ? Parce que HealthCheckService exécute les indicateurs en parallèle et doit distinguer « j'ai vérifié, c'est mauvais » de « la vérification elle-même a explosé ». Une HealthCheckError levée porte les deux : le signal d'échec et un statut prêt à placer dans la réponse.

Le troisième argument de getStatus est une donnée supplémentaire quelconque - elle atterrit dans details à côté du statut. C'est un bon endroit pour des nombres qui aideront au diagnostic : combien de légions, combien d'espace libre, combien de temps a duré la vérification.

Récapitulatif

L'appel est fait, la cohorte est apte au service :

  • un health check répond à la question de savoir si l'application est apte à travailler - un processus qui tourne ne le garantit pas,
  • le paquet dédié est @nestjs/terminus ; @nestjs/health, @nestjs/diagnostics et @nestjs/monitor n'existent pas,
  • HealthCheckService exécute un ensemble d'indicateurs et renvoie le statut agrégé - il ne redémarre pas l'application et n'envoie pas d'alertes,
  • l'ordre : this.health.check([, les indicateurs, ]),
  • tu passes chaque indicateur comme une fonction () => ... pour que le service puisse le lancer lui-même,
  • TypeOrmHealthIndicator vérifie la base de données, HttpHealthIndicator une API externe, MemoryHealthIndicator la mémoire, DiskHealthIndicator le disque,
  • la réponse du général au particulier : status (un mot), info (les seuls sains), details (tous),
  • un indicateur maison hérite de HealthIndicator et implémente isHealthy(key),
  • succès : return this.getStatus(key, true) ; échec : throw new HealthCheckError(...) - l'exception distingue une panne d'une vérification cassée,
  • les données supplémentaires de getStatus atterrissent dans details et servent au diagnostic.

Dans la prochaine leçon, nous passerons d'un appel unique à l'observation continue - tu découvriras les métriques et les systèmes d'alerte précoce. En attendant, retiens ceci : un processus vivant n'est pas la même chose qu'une application apte au service - et seul l'appel en décide.

Code de cette leçon : src/health/health-checks.ts
1// Health Checks - Diagnostyka Stanu Imperium
2// Surveillance de la santé de l'application avec @nestjs/terminus
3import { Injectable, Logger } from '@nestjs/common';
4import { Controller, Get } from '@nestjs/common';
5
6// ===========================================
7// 1. Health Indicator - indicateur de santé
8// ===========================================
9
10@Injectable()
11export class DatabaseHealthIndicator {
12  private logger = new Logger('DBHealth');
13
14  async isHealthy(): Promise<{
15    status: 'up' | 'down';
16    details: Record<string, any>;
17  }> {
18    try {
19      // Simulation d'une vérification de connexion à la base de données
20      const startTime = Date.now();
21      // await this.connection.query('SELECT 1');
22      const responseTime = Date.now() - startTime;
23
24      this.logger.log('Database: OK (' + responseTime + 'ms)');
25
26      return {
27        status: 'up',
28        details: {
29          responseTime: responseTime + 'ms',
30          connections: 5,
31          maxConnections: 20,
32        },
33      };
34    } catch (error) {
35      this.logger.error('Database: BLAD - ' + error.message);
36      return {
37        status: 'down',
38        details: { error: error.message },
39      };
40    }
41  }
42}
43
44@Injectable()
45export class MemoryHealthIndicator {
46  isHealthy() {
47    const used = process.memoryUsage();
48    const heapUsedMB = Math.round(used.heapUsed / 1024 / 1024);
49    const heapTotalMB = Math.round(used.heapTotal / 1024 / 1024);
50    const usagePercent = Math.round((heapUsedMB / heapTotalMB) * 100);
51
52    return {
53      status: usagePercent < 90 ? 'up' : 'down',
54      details: {
55        heapUsed: heapUsedMB + 'MB',
56        heapTotal: heapTotalMB + 'MB',
57        usage: usagePercent + '%',
58      },
59    };
60  }
61}
62
63// ===========================================
64// 2. Health Controller
65// ===========================================
66
67@Controller('health')
68export class HealthController {
69  constructor(
70    private dbHealth: DatabaseHealthIndicator,
71    private memoryHealth: MemoryHealthIndicator,
72  ) {}
73
74  @Get()
75  async check() {
76    const db = await this.dbHealth.isHealthy();
77    const memory = this.memoryHealth.isHealthy();
78
79    const overallStatus = db.status === 'up' && memory.status === 'up'
80      ? 'ok' : 'error';
81
82    return {
83      status: overallStatus,
84      info: {
85        database: db,
86        memory: memory,
87      },
88      timestamp: new Date().toISOString(),
89    };
90  }
91
92  @Get('liveness')
93  liveness() {
94    return { status: 'ok', uptime: process.uptime() };
95  }
96
97  @Get('readiness')
98  async readiness() {
99    const db = await this.dbHealth.isHealthy();
100    return {
101      status: db.status === 'up' ? 'ready' : 'not_ready',
102      database: db,
103    };
104  }
105}
106
107console.log('=== Health Checks ===');
108console.log("/health - état complet de l'application");
109console.log("/health/liveness - l'application est-elle vivante");
110console.log('/health/readiness - est-elle prête à recevoir du trafic');
111console.log('HealthIndicator - vérifie un composant spécifique');
112

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. Quel package NestJS est utilisé pour implémenter les Health Checks ?

  2. 2. Que fait HealthCheckService de @nestjs/terminus ?

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 HealthController avec @Get('/health') qui utilise HealthCheckService.check() avec TypeOrmHealthIndicator.pingCheck('database') et HttpHealthIndicator.pingCheck('api', 'http://localhost:4000')

  • Clique dans l'ordre

    Ordonne correctement les éléments de l'appel HealthCheckService.check()

  • Éditeur de code

    Complète la classe LegionHealthIndicator étendant HealthIndicator avec une méthode isHealthy(key) qui renvoie this.getStatus(key, true) en cas de succès ou lève une HealthCheckError en cas d'échec

  • Tri vertical

    Ordonne les champs de la réponse d'un Health Check de l'état global aux détails