Kurs NestJS · Moduł 6: Obsługa błędów i monitoring

Health Checks - diagnostyka stanu

5 min czytania
W tej lekcji6

Aplikacja wstała, proces działa, port odpowiada. Ale czy naprawdę jest zdolna do służby? Baza mogła nie przyjąć połączenia, dysk może być pełen, zewnętrzne API może milczeć. Proces żyje, a każde żądanie i tak skończy się błędem.

Rzymski obóz miał na to poranny apel. Nie liczono, ilu legionistów oddycha - sprawdzano po kolei: czy studnia daje wodę, czy magazyn nie jest pusty, czy posłaniec z sąsiedniego fortu dotarł. Dopiero suma tych odpowiedzi mówiła, czy kohorta może wymaszerować. Tym jest health check.

Pakiet i jego serce

W NestJS służy do tego pakiet @nestjs/terminus. Nazwa jest nieoczywista - nie ma tam słowa „health" - więc łatwo szukać czegoś w rodzaju @nestjs/health czy @nestjs/monitor. Takie pakiety nie istnieją.

Jego głównym elementem jest HealthCheckService, który uruchamia zestaw wskaźników i zwraca zagregowany status aplikacji. Sam niczego nie mierzy i - co ważne - nie restartuje aplikacji ani nie wysyła alertów. Zbiera odpowiedzi i podaje wynik; co z nim zrobić, decyduje ten, kto pyta.

Apel poranny

Kontroler zdrowia wygląda tak:

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}

Kolejność wywołania jest zawsze ta sama: this.health.check([ otwiera listę, wewnątrz stoją poszczególne wskaźniki, a ]) ją zamyka.

Zwróć uwagę, że każdy wskaźnik podajesz jako funkcję, nie jako wynik jej wywołania. Zapis () => this.db.pingCheck('database') pozwala serwisowi uruchomić sprawdzenia samodzielnie - równolegle i z obsługą błędów. Gdybyś napisał this.db.pingCheck('database') bez strzałki, sprawdzenie wykonałoby się natychmiast, poza kontrolą serwisu, a rzucony wyjątek wywróciłby cały endpoint.

Pierwszy argument każdego wskaźnika to klucz - nazwa, pod którą wynik pojawi się w odpowiedzi. To Ty ją wybierasz.

Wskaźniki wbudowane

Terminus daje kilka gotowych wskaźników, po jednym na typ zasobu:

  • TypeOrmHealthIndicator - sprawdza połączenie z bazą danych metodą pingCheck.
  • HttpHealthIndicator - odpytuje zewnętrzny adres HTTP, żeby sprawdzić, czy cudze API odpowiada.
  • MemoryHealthIndicator - pilnuje zużycia pamięci; checkHeap przyjmuje próg w bajtach.
  • DiskHealthIndicator - sprawdza wolne miejsce na dysku.

Nazwy są na tyle podobne, że warto je zapamiętać po zasobie, którego dotyczą: TypeOrm - baza, Http - cudze API, Memory - pamięć, Disk - dysk.

Kształt odpowiedzi

Wynik apelu ma trzy poziomy szczegółowości, od ogółu do szczegółu:

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

status to jedno słowo dla całości - 'ok', gdy wszystko przeszło, albo 'error', gdy cokolwiek zawiodło. To jego czyta system, który co kilkanaście sekund pyta o zdrowie aplikacji.

info zawiera tylko sprawne wskaźniki, a bliźniacze pole error - tylko te, które zawiodły. details to komplet: wszystkie wskaźniki niezależnie od wyniku.

Podział ma sens praktyczny: człowiek zagląda w details, żeby zobaczyć pełny obraz, a automat czyta status, bo tylko ta jedna wartość decyduje o akcji.

Własny wskaźnik

Wbudowane wskaźniki pilnują infrastruktury. Gdy chcesz sprawdzić coś z własnej dziedziny - czy skarbiec przyjmuje wpłaty, czy legion ma komplet - piszesz wskaźnik sam:

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      'Brak aktywnych legionów',
16      this.getStatus(key, false, { activeLegions: 0 }),
17    );
18  }
19}

Klasa dziedziczy po HealthIndicator i implementuje metodę isHealthy(key). Kontrakt jest dwustronny i warto go zapamiętać: przy sukcesie zwracasz this.getStatus(key, true), a przy niepowodzeniu rzucasz HealthCheckError z takim samym statusem, tylko z false.

Dlaczego wyjątek zamiast zwrócenia false? Bo HealthCheckService uruchamia wskaźniki równolegle i musi odróżnić „sprawdziłem, jest źle" od „sprawdzenie samo się wywaliło". Rzucony HealthCheckError niesie oba: informację o awarii i gotowy status do wstawienia w odpowiedź.

Trzeci argument getStatus to dowolne dane dodatkowe - trafią do details obok statusu. To dobre miejsce na liczby, które pomogą przy diagnozie: ile legionów, ile wolnego miejsca, jak długo trwało sprawdzenie.

Podsumowanie

Apel odbyty, kohorta zdolna do służby:

  • health check odpowiada, czy aplikacja jest zdolna do pracy - działający proces tego nie gwarantuje,
  • służy do tego pakiet @nestjs/terminus; @nestjs/health, @nestjs/diagnostics i @nestjs/monitor nie istnieją,
  • HealthCheckService uruchamia zestaw wskaźników i zwraca zagregowany status - nie restartuje aplikacji ani nie wysyła alertów,
  • kolejność: this.health.check([, wskaźniki, ]),
  • każdy wskaźnik podajesz jako funkcję () => ..., żeby serwis mógł uruchomić go sam,
  • TypeOrmHealthIndicator sprawdza bazę danych, HttpHealthIndicator - zewnętrzne API, MemoryHealthIndicator - pamięć, DiskHealthIndicator - dysk,
  • odpowiedź od ogółu do szczegółu: status (jedno słowo), info (tylko sprawne), details (wszystkie),
  • własny wskaźnik dziedziczy po HealthIndicator i implementuje isHealthy(key),
  • sukces: return this.getStatus(key, true); porażka: throw new HealthCheckError(...) - wyjątek pozwala odróżnić awarię od nieudanego sprawdzenia,
  • dane dodatkowe z getStatus lądują w details i przydają się przy diagnozie.

W następnej lekcji przejdziemy od pojedynczego apelu do stałej obserwacji - poznasz metryki i systemy wczesnego ostrzegania. A na razie zapamiętaj: proces, który żyje, to nie to samo co aplikacja zdolna do służby - i tylko apel to rozstrzyga.

Kod do tej lekcji: src/health/health-checks.ts
1// Health Checks - Diagnostyka Stanu Imperium
2// Monitorowanie zdrowia aplikacji z @nestjs/terminus
3import { Injectable, Logger } from '@nestjs/common';
4import { Controller, Get } from '@nestjs/common';
5
6// ===========================================
7// 1. Health Indicator - wskaznik zdrowia
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      // Symulacja sprawdzenia polaczenia z baza
20      const startTime = Date.now();
21      // await this.connection.query('SELECT 1');
22      const responseTime = Date.now() - startTime;
23
24      this.logger.log('Baza danych: 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('Baza danych: 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 - pelny status aplikacji');
109console.log('/health/liveness - czy aplikacja zyje');
110console.log('/health/readiness - czy gotowa na ruch');
111console.log('HealthIndicator - sprawdza konkretny komponent');
112

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaki pakiet NestJS służy do implementacji Health Checks?

  2. 2. Co robi HealthCheckService z @nestjs/terminus?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij HealthController z @Get('/health'), który używa HealthCheckService.check() z TypeOrmHealthIndicator.pingCheck('database') i HttpHealthIndicator.pingCheck('api', 'http://localhost:4000')

  • Klikanie w kolejności

    Ułóż elementy wywołania HealthCheckService.check() w poprawnej kolejności

  • Edytor kodu

    Uzupełnij klasę LegionHealthIndicator rozszerzającą HealthIndicator, z metodą isHealthy(key) która zwraca this.getStatus(key, true) przy sukcesie lub rzuca HealthCheckError przy błędzie

  • Układanie w pionie

    Ułóż pola odpowiedzi Health Check od statusu ogólnego do szczegółów

Przydatne artykuły