Skip to content

HealthModule

Module in @evolve-framework/core

Reports this service's health as one field of a Health entity shared with every other service in the composition.

Compose it into each service that should be reportable -- every instance of every service, since the check answers for the instance that received the query -- giving each one its own field name:

new HealthModule({
	name: "products",
	checks: {
		engine: () => client.ping(),
		cache: () => cache.get("health"),
	},
});

A single query then reports the whole deployment, which is what makes this usable as a promotion gate:

{ health { products { ok release } checkout { ok release } } }

Two HTTP routes carry the same information for callers that are not the router, and they answer different questions:

  • /healthcheck runs no checks and answers 200 with the component name and the running release for as long as the process can serve a request. This is the liveness probe, and the reason it checks nothing is that a probe fails on a timeout as surely as on a status code.
  • /readiness runs the checks and answers 503 when any of them failed. This is the one for a startup probe and for a deploy gate that can only read a status code.

A service that serves no GraphQL at all -- a payment provider, a sitemap builder, a search indexer -- composes this with graphql: false and gets the routes without becoming a subgraph.

This is deliberately not the router's own liveness or readiness probe: those answer for the router's HTTP layer and its selected supergraph, never reach a service, and keep reporting healthy while a service behind them is broken.

The report carries the running build so a gate can tell it reached the instances it is about to promote rather than the ones still serving traffic.

import { HealthModule } from "@evolve-framework/core";
new HealthModule(options: HealthModuleOptions)
OptionTypeDescription
namestring

The field this service contributes to the shared Health entity, e.g. products or checkout. It must be unique across the composition: two services contributing the same field name is a composition conflict, not a merge.

release?string

The build this instance runs, reported as release. Defaults to COMPONENT_VERSION, then GIT_SHA, then SENTRY_RELEASE, then unknown.

check?HealthCheck

Whether the service itself is healthy, for a reason that is not one of its dependencies. Its failure message lands on ServiceHealth.message.

checks?Record<string, HealthCheck | HealthCheckDefinition>

The dependencies to check, keyed by the name to report them under. They run concurrently on every query.

More

A bare function is reported without counting towards the verdict. Wrap it as { check, required: true } for a dependency the service cannot serve without, whose failure then sets ok to false and answers 503 on the readiness route.

checks: {
	engine: { check: () => client.ping(), required: true },
	cache: () => cache.get("health"),
}
readinessRoute?string | false

Where to serve the full report over plain HTTP, or false not to serve it. Defaults to /readiness.

More

It runs the checks and answers 200 when the service is healthy, 503 when it is not -- so unlike the GraphQL query it can be gated on a status code, where a federated response reports a partial failure as a 200 with a populated errors list. Point startup probes and deploy gates here.

Not a liveness probe. It reports on the service's dependencies, so a dependency outage answers 503 and would have every instance restarted for a fault none of them can fix. Liveness belongs on livenessRoute.

A readiness probe is worth a thought too, for the same reason in a quieter form: an outage of something every instance shares takes the whole deployment out of rotation, which turns a degraded service into an absent one. What this route is unambiguously right for is a startup probe -- invalid credentials or a missing scope then keep a new build from ever receiving traffic, which is the failure it can actually rule out.

livenessRoute?string | false

Where to serve the cheap liveness report, or false not to serve it. Defaults to /healthcheck, which is where the GraphQL server serves its own shallow probe, so a deployment that already points liveness there needs no change.

More

It answers 200 with the component name and the running release for as long as this process can serve a request, and deliberately runs no checks: a probe fails on a timeout as surely as on a status code, so a route meant to say "this process is alive" must not wait on anything that can hang.

graphql?boolean

Whether to contribute the Health entity to the service's GraphQL schema. Defaults to true.

More

Set it to false in a service that serves no GraphQL -- a payment provider, a sitemap builder, a search indexer. The module then contributes the HTTP routes only, where contributing type definitions would have made Application demand graphql options and turn the service into a subgraph.

timeout?number

How long a single check may take before it counts as failed, in milliseconds. Defaults to 2000.

More

A check that never settles is the case this exists for: without it a hung dependency hangs the health query itself, and a deploy gate waiting on the query learns nothing until the router's own timeout fires.

Extends AbstractModule<unknown>.