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.
W NestJS służy do tego pakiet
. Nazwa jest nieoczywista - nie ma tam słowa „health" - więc łatwo szukać czegoś w rodzaju @nestjs/terminus
@nestjs/health czy @nestjs/monitor. Takie pakiety nie istnieją.Jego głównym elementem jest
, 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.HealthCheckService
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:
otwiera listę, wewnątrz stoją poszczególne wskaźniki, a this.health.check([
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.
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.
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}
to jedno słowo dla całości - status
'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.
zawiera tylko sprawne wskaźniki, a bliźniacze pole info
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.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
i implementuje metodę HealthIndicator
. Kontrakt jest dwustronny i warto go zapamiętać: przy sukcesie zwracasz isHealthy(key)
, a przy niepowodzeniu rzucasz this.getStatus(key, true)
z takim samym statusem, tylko z HealthCheckError
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, @name: ile legionów, ile wolnego miejsca, jak długo trwało sprawdzenie.Apel odbyty, kohorta zdolna do służby:
@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,this.health.check([, wskaźniki, ]),() => ..., żeby serwis mógł uruchomić go sam,TypeOrmHealthIndicator sprawdza bazę danych, HttpHealthIndicator - zewnętrzne API, MemoryHealthIndicator - pamięć, DiskHealthIndicator - dysk,status (jedno słowo), info (tylko sprawne), details (wszystkie),HealthIndicator i implementuje isHealthy(key),return this.getStatus(key, true); porażka: throw new HealthCheckError(...) - wyjątek pozwala odróżnić awarię od nieudanego sprawdzenia,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.