Originally posted on my blog at https://elliotdenolf.com/posts/standardized-health-checks-in-typescript
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 asHealthy
- Must implement the
checkHealth()
function, which has the ability to modify thestatus
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 areHealthy
. If any single dependency isUnhealthy
, the entire health will be set toUnhealthy
. The overall health and all downstream dependencies are returned in theHealthCheckResult
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 theHealthService
. - 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:
Top comments (1)
Routine Health check is very important to humans healthy life. Today everybody have a app in their mobile phones who can keep the complete record of human health on week or monthly basis. A origination ndis funding to start a business plays an important role in human life to assist with these kinds of apps.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.