Kurs NestJS · Moduł 6: Obsługa błędów i monitoring
Health Checks - diagnostyka stanu
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;checkHeapprzyjmuje 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/diagnosticsi@nestjs/monitornie istnieją, HealthCheckServiceuruchamia 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, TypeOrmHealthIndicatorsprawdza 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
HealthIndicatori implementujeisHealthy(key), - sukces:
return this.getStatus(key, true); porażka:throw new HealthCheckError(...)- wyjątek pozwala odróżnić awarię od nieudanego sprawdzenia, - dane dodatkowe z
getStatuslądują wdetailsi 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');
112Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki pakiet NestJS służy do implementacji Health Checks?
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