first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# @suaveplan/health-check - Server-tier standardised /health, /ready, /live endpoints with dependency checks — HealthChecker/ResponseBuilder/HttpCheck/TcpCheck with injectable Clock+TimersLike+LoggerLike+fetch, HEALTH_CHECK_CODES error registry, and OTel self-telemetry.
Server-tier health-check core for `/health`, `/ready`, `/live` endpoints with
dependency checks (HTTP, TCP, custom), aggregated status, caching, and
typed lifecycle events.
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` provides the canonical error namespace for `@suaveplan/health-check`. Every public failure path — invalid…
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` provides the canonical error namespace for `@suaveplan/health-check`. Every public failure path — invalid…
- [health-checker](./src/health-checker/health-checker.md) — `HealthChecker` is the orchestration core of `@suaveplan/health-check`. It owns a registry of dependency checks, runs t…
- [health-checker.helpers](./src/health-checker/health-checker.helpers.md) — `health-checker.helpers` carries the pure helpers that back the `HealthChecker` class: the Zod schemas that validate co…
- [health-checker](./src/health-checker/health-checker.md) — `HealthChecker` is the orchestration core of `@suaveplan/health-check`. It owns a registry of dependency checks, runs t…
- [http-check](./src/http-check/http-check.md) — `createHttpCheck` returns a `CheckFn` that probes an HTTP endpoint and reports `healthy` when the response status appea…
- [http-check](./src/http-check/http-check.md) — `createHttpCheck` returns a `CheckFn` that probes an HTTP endpoint and reports `healthy` when the response status appea…
- [response-builder](./src/response-builder/response-builder.md) — `ResponseBuilder` adapts a `HealthChecker` into HTTP-shaped responses suitable for direct return from a Hono / Express…
- [response-builder](./src/response-builder/response-builder.md) — `ResponseBuilder` adapts a `HealthChecker` into HTTP-shaped responses suitable for direct return from a Hono / Express…
- [tcp-check](./src/tcp-check/tcp-check.md) — `createTcpCheck` returns a `CheckFn` that opens a TCP socket against a `host:port` pair and resolves to `healthy` once…
- [tcp-check](./src/tcp-check/tcp-check.md) — `createTcpCheck` returns a `CheckFn` that opens a TCP socket against a `host:port` pair and resolves to `healthy` once…
- [telemetry](./src/telemetry/telemetry.md) — `telemetry` registers the OpenTelemetry meter that the health-check package emits to on every public call. The instrume…
- [telemetry](./src/telemetry/telemetry.md) — `telemetry` registers the OpenTelemetry meter that the health-check package emits to on every public call. The instrume…
<!-- end:auto-docs -->
## Modules
- [`health-checker/`](./src/health-checker/health-checker.md) — `HealthChecker`: registry, runner, aggregator, `EventBus`.
- [`http-check/`](./src/http-check/http-check.md) — `createHttpCheck`: fetch-based probe with injectable `fetchImpl`.
- [`tcp-check/`](./src/tcp-check/tcp-check.md) — `createTcpCheck`: socket probe with injectable `TcpConnectFn`.
- [`response-builder/`](./src/response-builder/response-builder.md) — `ResponseBuilder`: maps statuses to HTTP codes.
- [`error-codes/`](./src/error-codes/error-codes.md) — `HEALTH_CHECK_CODES` + `HealthCheckError`.
- [`telemetry/`](./src/telemetry/telemetry.md) — OTel meter (counter + histogram).
## Install
```bash
bun add @suaveplan/health-check
```
## Usage
```ts
import {
HealthChecker,
ResponseBuilder,
createHttpCheck,
createTcpCheck,
} from "@suaveplan/health-check";
const checker = new HealthChecker({ name: "api", version: "1.0.0" });
checker.addCheck({
name: "upstream",
check: createHttpCheck({ url: "https://up.example.com/health" }),
});
checker.addCheck({
name: "postgres",
check: createTcpCheck({ host: "db.internal", port: 5432 }),
});
const builder = new ResponseBuilder(checker);
const { statusCode, body } = await builder.health();
```
## License
MIT

View File

@@ -0,0 +1,57 @@
# `error-codes/error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`error-codes` provides the canonical error namespace for `@suaveplan/health-check`. Every public failure path — invalid options, duplicate registration, unknown look-ups, per-check timeouts, and unexpected check-function throws — surfaces as a `HealthCheckError` whose `code` is drawn from the `HEALTH_CHECK_CODES` registry. Consumers SHOULD discriminate on `error.code` rather than `instanceof` so the namespace survives module-boundary copies and JSON serialisation.
## Features
- Stable namespaced codes built via `@suaveplan/error`'s `defineCodes("HEALTH_CHECK", …)`.
- Typed `HealthCheckCode` literal union narrows `switch` blocks at compile time.
- `HealthCheckError` extends `SuaveplanError<HealthCheckCode>` with a **public** constructor that delegates to the protected base, so call-site `new HealthCheckError(...)` works.
- `cause` + `context` carried through the inherited options object.
## Usage: Basic
```ts
import {
HEALTH_CHECK_CODES,
HealthCheckError,
} from "@suaveplan/health-check";
throw new HealthCheckError(
HEALTH_CHECK_CODES.UNKNOWN_CHECK,
"check \"db\" is not registered",
{ context: { name: "db" } },
);
```
## Usage: Advanced
```ts
try {
await checker.runCheck("db");
} catch (err) {
if (
err instanceof HealthCheckError &&
err.code === HEALTH_CHECK_CODES.CHECK_TIMEOUT
) {
metrics.increment("health.timeout", { ctx: err.context });
}
throw err;
}
```
## API Reference
- `HEALTH_CHECK_CODES` — frozen map: `INVALID_OPTIONS`, `DUPLICATE_CHECK`, `UNKNOWN_CHECK`, `CHECK_TIMEOUT`, `CHECK_EXECUTION_FAILED`.
- `HealthCheckCode` — the literal-union type derived from the registry values.
- `HealthCheckError(code, message, options?)` — `options` carries `cause` and `context`.
## Implementation Notes
`defineCodes` namespaces every leaf under `HEALTH_CHECK.` and freezes the registry to prevent accidental mutation. `HealthCheckError`'s public constructor exists only because `SuaveplanError` declares its constructor `protected` — without an explicit override the call-site `new HealthCheckError(...)` would not type-check. The constructor sets `this.name = "HealthCheckError"` so logs / traces show a meaningful class name even after structured clone.

View File

@@ -0,0 +1,49 @@
# `health-checker/health-checker.helpers`
## Purpose
`health-checker.helpers` carries the pure helpers that back the `HealthChecker` class: the Zod schemas that validate constructor + per-check options, the default `Clock` / `TimersLike` pair, the aggregate-status function, the raw-result normaliser, and the timeout-enforcing wrapper. They are split out of `health-checker.ts` to keep the main class file inside the project line-count budget and to make each helper independently unit-testable through `HealthChecker`'s tests.
## Features
- `dependencyCheckSchema` + `optionsSchema` — Zod boundary schemas; throw `HealthCheckError(INVALID_OPTIONS)` on failure when wired in.
- `defaultClock` — `() => Date.now()`.
- `defaultTimers` — `setTimeout` / `clearTimeout` adapter for the `TimersLike` contract.
- `aggregateStatus(results, isCritical)` — pure function mapping a result list to overall `CheckStatus`.
- `normalizeResult(name, raw, elapsed, clock)` — pure normaliser preserving raw `name` / `timestamp` when present.
- `executeWithTimeout(thunk, ms, timers)` — schedules a `HealthCheckError(CHECK_TIMEOUT)` rejection through the supplied `TimersLike`.
## Usage: Basic
```ts
import { aggregateStatus } from "@suaveplan/health-check/health-checker";
const status = aggregateStatus(results, (name) => criticalNames.has(name));
```
## Usage: Advanced
```ts
import { executeWithTimeout } from "@suaveplan/health-check/health-checker";
const result = await executeWithTimeout(
() => probe(),
250,
{ setTimer: setTimeout, clearTimer: clearTimeout },
);
```
## API Reference
- `DEFAULT_TIMEOUT: 5000`
- `defaultClock: Clock`
- `defaultTimers: TimersLike`
- `dependencyCheckSchema: ZodObject`
- `optionsSchema: ZodObject`
- `aggregateStatus(results, isCritical) → CheckStatus`
- `normalizeResult(name, raw, elapsed, clock) → CheckResult`
- `executeWithTimeout<T>(thunk, ms, timers) → Promise<T>`
## Implementation Notes
`executeWithTimeout` carries a `settled` flag to absorb the race between the timer firing and the thunk resolving. Either path is responsible for flipping the flag; the other path no-ops. `clearTimer` is only called on the resolution / rejection side because the timer-fired side has already executed and there is nothing to cancel. Rejections from the thunk are coerced into `HealthCheckError(CHECK_EXECUTION_FAILED)` so the outer `runCheck` always sees a typed error and can route on `code` instead of `instanceof Error`. The Zod schemas are constructed at module top level — instantiating them once per import keeps the per-call overhead to a single `safeParse` invocation.

View File

@@ -0,0 +1,65 @@
# `health-checker/health-checker`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`HealthChecker` is the orchestration core of `@suaveplan/health-check`. It owns a registry of dependency checks, runs them on demand, enforces per-check timeouts through an injected `TimersLike`, caches results for a configurable TTL, and produces standardised health, readiness, and liveness payloads suitable for the `/health`, `/ready`, and `/live` endpoints common to Kubernetes, ECS, Nomad, and reverse-proxy probes. Wall-clock readings come from the injected `Clock`, so tests can drive deterministic uptime + cache-expiry behaviour.
## Features
- Registry of `DependencyCheckConfig` with per-check `critical`, `timeout`, and `cacheTtlMs` overrides.
- Result aggregation that returns `healthy` / `degraded` / `unhealthy`: critical failures short-circuit, non-critical or degraded results downgrade to `degraded`.
- Typed `EventBus<HealthEventMap>` emitting `check:passed`, `check:failed`, and `status:changed` events.
- Zod-validated boundaries — every option object is parsed before use; failures throw `HealthCheckError` with `INVALID_OPTIONS`.
- OTel instrumentation: every check run records on `health_check_runs_total` + `health_check_duration_ms`, and every aggregate evaluation increments `health_check_aggregate_status_total`.
- Optional `LoggerLike` injection for `debug` registration logs and `warn` on failed checks.
## Usage: Basic
```ts
import { HealthChecker, createHttpCheck } from "@suaveplan/health-check";
const checker = new HealthChecker({ name: "api", version: "1.0.0" });
checker.addCheck({
name: "upstream",
check: createHttpCheck({ url: "https://up.example.com/health" }),
});
const response = await checker.health();
```
## Usage: Advanced
```ts
import { HealthChecker, HealthEvent } from "@suaveplan/health-check";
const checker = new HealthChecker({
name: "api",
defaultTimeout: 2_000,
defaultCacheTtlMs: 5_000,
clock: () => performance.now() + epoch,
timers: fakeTimers,
logger,
});
checker.events.on(HealthEvent.STATUS_CHANGED, ({ previous, current }) => {
pagerduty.notify({ previous, current });
});
```
## API Reference
- `new HealthChecker(options)` — `options.name` required; `version`, `defaultTimeout`, `defaultCacheTtlMs`, `checks`, `clock`, `timers`, `logger` optional.
- `addCheck(config)` / `removeCheck(name)` / `getCheckNames()`.
- `runCheck(name)` → `CheckResult` (single check, honours cache).
- `health()` → `HealthResponse` (runs all, aggregates).
- `ready()` → `ReadinessResponse` (critical-only failures).
- `live()` → `LivenessResponse` (process is responding).
- `clearCache()` / `getUptimeMs()`.
- `events: EventBus<HealthEventMap>`.
## Implementation Notes
Timeout enforcement is delegated to the injected `TimersLike` rather than `setTimeout` so tests can drive timeouts deterministically without `vi.useFakeTimers()`. The cache is keyed by check name and stores a frozen `CheckResult` with an absolute `expiresAt` computed against `this.clock()` — clock skew between writes and reads cannot leak negative TTLs because we re-read the clock on every check. Cached reads tag results with `cached: true` so consumers can distinguish them. Failures inside `executeWithTimeout` are wrapped in `HealthCheckError(CHECK_EXECUTION_FAILED)` so the outer `runCheck` always sees a typed error; the `CHECK_TIMEOUT` branch is preserved separately so it can be surfaced in `details.timedOut`.

View File

@@ -0,0 +1,54 @@
# `http-check/http-check`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createHttpCheck` returns a `CheckFn` that probes an HTTP endpoint and reports `healthy` when the response status appears in `expectedStatusCodes`. It is the standard building block for "is upstream X reachable?" probes — used to check downstream APIs, gateways, internal dependency surfaces, and anything else with a `/health` or `/status` endpoint. The factory accepts an injectable `fetchImpl` so tests never touch real network sockets, and an injectable `Clock` so `responseTime` is deterministic.
## Features
- Method, headers, and expected-status-code overrides via the `HttpCheckConfig` argument.
- AbortController-driven per-request timeout that maps to `responseTime` + the human-readable `"HTTP check timed out after Xms"` message.
- Network errors are returned as `unhealthy` with `details.error` containing the raw message — not thrown.
- Zod boundary validation: empty `url`, empty `method`, non-positive `timeout` all throw `HealthCheckError(INVALID_OPTIONS)`.
- `fetchImpl` defaults to `globalThis.fetch` bound to `globalThis`; tests pass a stub.
## Usage: Basic
```ts
import { createHttpCheck } from "@suaveplan/health-check";
const check = createHttpCheck({ url: "https://upstream.example/health" });
const result = await check();
```
## Usage: Advanced
```ts
import { createHttpCheck } from "@suaveplan/health-check";
const check = createHttpCheck(
{
url: "https://internal.api/health",
method: "HEAD",
headers: { authorization: `Bearer ${token}` },
timeout: 1_500,
expectedStatusCodes: [200, 204],
fetchImpl: instrumentedFetch,
},
{ clock: () => performance.now() },
);
```
## API Reference
- `createHttpCheck(config, options?)` → `CheckFn`.
- `config: HttpCheckConfig` — `url` required; `method`, `headers`, `timeout`, `expectedStatusCodes`, `fetchImpl` optional.
- `options.clock?: Clock` — overrides `Date.now` for response-time measurement.
## Implementation Notes
The check never re-throws. Network errors, abort errors, and unexpected status codes are all funnelled into `CheckResult` objects so the `HealthChecker` can record them uniformly. Abort detection is case-insensitive on `"abort"` / `"timeout"` and also consults `controller.signal.aborted` so adapters that surface only a generic error message still classify correctly. The `responseTime` is measured between the clock readings around `fetchImpl` so it includes DNS, TCP, TLS, and the server's processing time — matching how operators read latency dashboards. The factory is pure: it validates options once and returns a closure that captures the normalised config, so the per-call cost is just the fetch + a small amount of bookkeeping.

View File

@@ -0,0 +1,64 @@
# `response-builder/response-builder`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ResponseBuilder` adapts a `HealthChecker` into HTTP-shaped responses suitable for direct return from a Hono / Express / Fastify handler. It maps the abstract `CheckStatus` union to concrete HTTP status codes following the conventions baked into Kubernetes, ECS, and most load-balancer probes: `healthy` and `degraded` both produce `200` (so callers continue to receive traffic on degraded but still-serving services), `unhealthy` produces `503`, readiness produces `503` when any critical check is failing, and liveness always produces `200` so long as the process can run the handler.
## Features
- Three response factories — `health()`, `ready()`, `live()` — each returning `{ statusCode, body }`.
- Pure mapping: the builder never mutates the underlying checker.
- Status mapping is centralised in a single `statusToCode` switch so behaviour is auditable.
- Liveness is synchronous to keep `/live` cheap; readiness and health run the checks.
## Usage: Basic
```ts
import { HealthChecker, ResponseBuilder } from "@suaveplan/health-check";
const checker = new HealthChecker({ name: "api" });
const builder = new ResponseBuilder(checker);
app.get("/health", async (req, res) => {
const r = await builder.health();
res.status(r.statusCode).json(r.body);
});
```
## Usage: Advanced
```ts
import { Hono } from "hono";
import { HealthChecker, ResponseBuilder } from "@suaveplan/health-check";
const checker = new HealthChecker({ name: "api" });
const builder = new ResponseBuilder(checker);
const app = new Hono()
.get("/health", async (c) => {
const r = await builder.health();
return c.json(r.body, r.statusCode);
})
.get("/ready", async (c) => {
const r = await builder.ready();
return c.json(r.body, r.statusCode);
})
.get("/live", (c) => {
const r = builder.live();
return c.json(r.body, r.statusCode);
});
```
## API Reference
- `new ResponseBuilder(checker)` — accepts an existing `HealthChecker`.
- `health()` → `Promise<HttpHealthResponse<HealthResponse>>`.
- `ready()` → `Promise<HttpHealthResponse<ReadinessResponse>>`.
- `live()` → `HttpHealthResponse<LivenessResponse>`.
## Implementation Notes
Returning `200` for `degraded` is deliberate: a degraded service is still serving traffic — operators want it to remain in the load-balancer pool while alerting fires on the underlying check. Flipping a degraded service into the unreachable pool would amplify outages because non-critical dependency hiccups would drop the entire fleet. `live()` deliberately performs no work so it is safe to wire to a tight liveness-probe interval without amplifying load on the dependencies. The builder is intentionally a thin adapter so a future plugin could wrap the same `HealthChecker` in a different protocol (gRPC, JSON-RPC, etc.) without re-implementing the orchestration.

View File

@@ -0,0 +1,53 @@
# `tcp-check/tcp-check`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createTcpCheck` returns a `CheckFn` that opens a TCP socket against a `host:port` pair and resolves to `healthy` once the `connect` event fires. It complements `createHttpCheck` for layer-4 probes — useful for databases, message brokers, and proxies that do not expose an HTTP `/health` route. The connector is fully injectable through `TcpConnectFn`, so tests use a deterministic stub and production code uses `node:net.connect`.
## Features
- Injectable `TcpConnectFn`: tests pass a fake socket with controlled `connect` / `error` emission; production uses `node:net`.
- Timeout enforcement via `setTimeout` that resolves to an `unhealthy` `CheckResult` with `details.timedOut === true`.
- Late events (a stray `error` after `connect`) are ignored once the promise settles.
- Zod boundary validation: missing `host`, out-of-range `port`, non-positive `timeout` all throw `HealthCheckError(INVALID_OPTIONS)`.
- Always destroys the socket — connect, error, and timeout paths each call `socket.destroy()`.
## Usage: Basic
```ts
import { createTcpCheck } from "@suaveplan/health-check";
const check = createTcpCheck({ host: "db.internal", port: 5432 });
const result = await check();
```
## Usage: Advanced
```ts
import { createTcpCheck } from "@suaveplan/health-check";
const check = createTcpCheck(
{
host: "db.internal",
port: 5432,
timeout: 500,
connectImpl: instrumentedConnect,
},
{ clock: () => performance.now() },
);
```
## API Reference
- `createTcpCheck(config, options?)` → `CheckFn`.
- `config: TcpCheckConfig` — `host` + `port` required; `timeout`, `connectImpl` optional.
- `options.clock?: Clock` — overrides `Date.now` for response-time measurement.
- `TcpSocketLike` — minimal structural socket the check drives (`on("connect" | "error", cb)` + `destroy()`).
## Implementation Notes
The check resolves rather than rejects on every failure path so the `HealthChecker` can treat result production uniformly. Late events (a stray `error` after `connect` resolved) are absorbed by `Promise.resolve`'s idempotency — only the first `resolve()` call wins, so the call site can stay lock-free. The default `connectImpl` performs a runtime cast from `node:net`'s `Socket` to `TcpSocketLike`; the cast is sound because `Socket` exposes the `on("connect" | "error", …)` and `destroy()` methods unchanged. The `connectImpl` injection point means consumers can also drive this check against an alternative transport (Unix-domain sockets, mock fixtures, etc.) without touching the public API.

View File

@@ -0,0 +1,47 @@
# `telemetry/telemetry`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`telemetry` registers the OpenTelemetry meter that the health-check package emits to on every public call. The instruments are bound lazily through `metrics.getMeter("@suaveplan/health-check")` so the package can be imported in production builds that have not opted into OTel — the API returns a no-op meter when no provider is registered. Tests install a real in-memory `MeterProvider` in `tests/setup/polyfills.ts` before any source module imports, so the same imports observed in production are exercised in unit tests.
## Features
- Counter `health_check_runs_total` — tagged with `name` and `status` for every executed dependency check.
- Counter `health_check_aggregate_status_total` — tagged with the resulting overall status for every call to `HealthChecker.health()`.
- Histogram `health_check_duration_ms` — recorded with the `name` tag for every executed dependency check.
- Helper `record*` functions used by `HealthChecker` so the call-sites stay legible.
## Usage: Basic
```ts
import { recordCheckRun } from "@suaveplan/health-check";
recordCheckRun("db", "healthy");
```
## Usage: Advanced
```ts
import {
aggregateStatusCounter,
checkDurationHistogram,
} from "@suaveplan/health-check";
aggregateStatusCounter.add(1, { status: "degraded", region: "eu-west-1" });
checkDurationHistogram.record(latencyMs, { name: "db", region: "eu-west-1" });
```
## API Reference
- `recordCheckRun(name, status)` — increment `health_check_runs_total`.
- `recordAggregateStatus(status)` — increment `health_check_aggregate_status_total`.
- `recordCheckDuration(name, durationMs)` — record on `health_check_duration_ms`.
- Raw `Counter` / `Histogram` instruments are exported for callers that need bespoke labels.
## Implementation Notes
The meter is constructed at module top-level so its caching matches OTel's per-process semantics. Switching the global `MeterProvider` after import will not re-bind the meter — this matches OTel best practice and lets tests inject a provider before the module evaluates. The package emits **no** labels containing user data; all tag values are static enum strings (`healthy` / `degraded` / `unhealthy`) or operator-supplied check names. This keeps cardinality bounded and prevents accidental leakage.