We use cookies to enhance your experience on the site
CodeWorlds

Health Checks - diagnosing the state

The application started, the process runs, the port answers. But is it truly fit for duty? The database may have refused a connection, the disk may be full, an external API may be silent. The process lives, and every request will fail anyway.

A Roman camp had a morning roll call for this. Nobody counted how many legionaries were breathing - they checked in turn: does the well give water, is the store not empty, did the messenger from the neighbouring fort arrive. Only the sum of those answers said whether the cohort could march. That is a health check.

The package and its heart

In NestJS this is served by the

@nestjs/terminus
package. The name is unhelpful - there is no "health" in it - so it is easy to go looking for something like
@nestjs/health
or
@nestjs/monitor
. No such packages exist.

Its central element is

HealthCheckService
, which runs a set of indicators and returns the application's aggregated status. It measures nothing itself and - importantly - neither restarts the application nor sends alerts. It gathers answers and reports the result; what to do with it is for whoever asked.

The roll call

A health controller looks like this:

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}

The call's order is always the same:

this.health.check([
opens the list, the individual indicators stand inside, and
])
closes it.

Note that you pass each indicator as a function, not as the result of calling it. Writing

() => this.db.pingCheck('database')
lets the service run the checks itself - in parallel and with error handling. Had you written
this.db.pingCheck('database')
without the arrow, the check would run immediately, outside the service's control, and a thrown exception would take the whole endpoint down.

Each indicator's first argument is a key - the name under which its result appears in the response. You choose it.

The built-in indicators

Terminus provides several ready indicators, one per kind of resource:

  • TypeOrmHealthIndicator
    - checks the database connection with
    pingCheck
    .
  • HttpHealthIndicator
    - queries an external HTTP address to see whether somebody else's API answers.
  • MemoryHealthIndicator
    - watches memory usage;
    checkHeap
    takes a threshold in bytes.
  • DiskHealthIndicator
    - checks free disk space.

The names are similar enough that it is worth remembering them by the resource they concern: TypeOrm - database, Http - external API, Memory - memory, Disk - disk.

The shape of the response

The roll call's result has three levels of detail, from general to specific:

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

status
is one word for the whole -
'ok'
when everything passed, or
'error'
when anything failed. It is what a system asking about the application's health every dozen seconds reads.

info
contains only the healthy indicators, while its twin field
error
holds only the failing ones.
details
is the complete set: every indicator regardless of outcome.

The split has a practical point: a human looks into

details
for the full picture, while an automated system reads
status
, because that single value is what decides its action.

A custom indicator

The built-in indicators watch infrastructure. When you want to check something from your own domain - whether the treasury accepts deposits, whether a legion is at full strength - you write an indicator yourself:

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      'No active legions',
16      this.getStatus(key, false, { activeLegions: 0 }),
17    );
18  }
19}

The class extends

HealthIndicator
and implements the
isHealthy(key)
method. The contract has two sides and is worth remembering: on success you return
this.getStatus(key, true)
, and on failure you throw
HealthCheckError
carrying the same status, only with
false
.

Why an exception rather than returning

false
? Because
HealthCheckService
runs the indicators in parallel and must tell "I checked, it is bad" apart from "the check itself blew up". A thrown
HealthCheckError
carries both: the failure signal and a ready status to place in the response.

The third argument of

getStatus
is arbitrary extra data - it lands in
details
beside the status. That is a good place for numbers that will help a diagnosis, @name: how many legions, how much free space, how long the check took.

Summary

The roll call is done and the cohort is fit for duty:

  • a health check answers whether the application is fit to work - a running process does not guarantee that,
  • the package for it is
    @nestjs/terminus
    ;
    @nestjs/health
    ,
    @nestjs/diagnostics
    and
    @nestjs/monitor
    do not exist,
  • HealthCheckService
    runs a set of indicators and returns the aggregated status
    - it neither restarts the application nor sends alerts,
  • the order:
    this.health.check([
    , the indicators,
    ])
    ,
  • you pass each indicator as a function
    () => ...
    so the service can run it itself,
  • TypeOrmHealthIndicator
    checks the database
    ,
    HttpHealthIndicator
    an external API,
    MemoryHealthIndicator
    memory,
    DiskHealthIndicator
    the disk,
  • the response from general to specific:
    status
    (one word),
    info
    (healthy only),
    details
    (all of them),
  • a custom indicator extends
    HealthIndicator
    and implements
    isHealthy(key)
    ,
  • success:
    return this.getStatus(key, true)
    ; failure:
    throw new HealthCheckError(...)
    - the exception distinguishes a failure from a broken check,
  • extra data from
    getStatus
    lands in
    details
    and helps with diagnosis.

In the next lesson we will move from a single roll call to continuous observation - you will meet metrics and early warning systems. For now remember: a living process is not the same as an application fit for duty - and only the roll call settles that.

Go to CodeWorlds