Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Health Checks - diagnostyka stanu

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, @name: 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.

Przejdź do CodeWorlds