~/blog|
Published on

Health Checks for TypeScript Microservices

Authors

Assessing the health of your overall system is vital when working with microservices. Just because a service is up and running does not necessarily mean it is able to successfully service a request. Enter health checks. Health checks provide a way of evaluating if a service is not only up and running but also fully prepared to service requests. Each service exposes an endpoint that reveals the health of itself as well as any downstream dependencies. Some examples of possible dependencies are other microservices, a database connection, or a service's own configuration. If a service is deemed to be unhealthy, traffic can be routed elsewhere and the service can be rotated out.

This post will go through how to implement a standardized health check of a downstream dependency in an easily repeatable way for other types of dependencies.

Let's start off defining an abstract class that must be implemented by all health indicators and a ResourceHealth enum to represent each resource's health.

// health-indicator.ts
export abstract class HealthIndicator {
  abstract name: string;
  status: ResourceHealth = ResourceHealth.Unhealthy;
  details: string | undefined;

  abstract checkHealth(): Promise<void>;
}

// resource-health.enum.ts
export enum ResourceHealth {
  Healthy = 'HEALTHY',
  Unhealthy = 'UNHEALTHY'
}

Each health indicator:

  • Starts out in the Unhealthy state by default until it can be verified as Healthy
  • Must implement the checkHealth() function, which has the ability to modify the status

The downstream dependency that we will be verifying is a JSON api that exposes a /ping endpoint. Here is the implementation:

//  some-service.check.ts
export class SomeServiceCheck extends HealthIndicator {
  name: string = 'Some Service';

  async checkHealth(): Promise<void> {
    let result: AxiosResponse<any>;
    try {
      const pingURL = `http://localhost:8080/ping`;
      result = await axios(pingURL);

      if (result.status === 200) {
        this.status = ResourceHealth;
      } else {
        this.status = ResourceHealth.Unhealthy;
        this.details = `Received status: ${result.status}`;
      }
    } catch (e) {
      this.status = ResourceHealth.Unhealthy;
      this.details = e.message;
      console.log(`HEALTH: ${this.name} is unhealthy.`, e.message);
    }
  }
}

The checkHealth() implementation is using the axios library to perform a GET request against the /ping endpoint, then evaluates the status. If it is a 200, the status will be set to Healthy. If some other code is returned or an error occurs, the status will be set to Unhealthy and details property will be set.

Next, let's look at implementing a health check service that will be managing all different types of health indicators and executing them.


// health.service.ts
export class HealthService {
  private readonly checks: HealthIndicator[];
  public overallHealth: ResourceHealth = ResourceHealth.Healthy;

  constructor(checks: HealthIndicator[]) {
    this.checks = checks;
  }

  async getHealth(): Promise<HealthCheckResult> {
    await Promise.all(
      this.checks.map(check => check.checkHealth())
    );

    const anyUnhealthy = this.checks.some(item =>
      item.status === ResourceHealth.Unhealthy
    );
    this.overallHealth = anyUnhealthy
      ? ResourceHealth.Unhealthy
      : ResourceHealth.Healthy;

    return {
      status: this.overallHealth,
      results: this.checks
    };
  }
}

type HealthCheckResult = {
  status: ResourceHealth,
  results: HealthIndicator[]
};

The HealthService does the following things:

  • Receives all health indicators to be run in its constructor
  • Performs all health checks in a Promise.all() statement
  • Reports the overall health of the system. This is set to Healthy if all downstream dependencies are Healthy. If any single dependency is Unhealthy, the entire health will be set to Unhealthy. The overall health and all downstream dependencies are returned in the HealthCheckResult response.

The last piece will be calling this service from a /health route on our service. For this example, we will be calling the service from an express router which can be mounted via app.use(healthRoutes).

// health.routes.ts
const healthRoutes = Router();

healthRoutes.get('/health', async (req, res) => {
  const healthService = new HealthService(
    [
      new SomeServiceCheck(),
      // Add more checks here...
    ]
  );

  const healthResults = await healthService.getHealth();

  res.status(healthResults.status === ResourceHealth.Healthy ? 200 : 503)
    .send({
      status: healthResults.status, dependencies: healthResults.results
    });
});

export { healthRoutes };

When this route is hit, the HealthService will be created with any necessary health indicators, then run all of the checks via getHealth(). The top level status of response will be of type ResourceHealth, either Healthy or Unhealthy with an associated HTTP status code - 200 for healthy or 503 for unhealthy. It will also have a results property showing every dependency by name and the resulting health from the check.

Performing a curl against this route will return:

{
  "status": "HEALTHY",
  "dependencies": [
    {
      "name": "Some Service",
      "status": "HEALTHY"
    }
  ]
}

Further improvements beyond this example:

  • Additional health indicators can be added simply by creating a class that implements our HealthIndicator abstract class then passed into the HealthService.
  • If further checks need to be implemented using HTTP GET requests, another base class could be pulled out of the SomeServiceCheck in order to be re-used.
  • The HealthService should be implemented as a singleton if it is to be called from other parts of your code.

Links: