Files
suaveplan/openspec/specs/health-check/spec.md
2026-05-27 23:29:53 +01:00

64 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Health-Check — Spec Delta
## ADDED Requirements
### Requirement: HealthChecker registers, runs, and aggregates dependency checks
`HealthChecker` MUST expose `addCheck(config)`, `removeCheck(name)`, `getCheckNames()`, `runCheck(name)`, `health()`, `ready()`, `live()`, `clearCache()`, and `getUptimeMs()`. `addCheck` MUST throw `HealthCheckError(DUPLICATE_CHECK)` when the name is already registered. `runCheck` MUST throw `HealthCheckError(UNKNOWN_CHECK)` when the name is missing. `health()` MUST aggregate to `unhealthy` if any critical check is unhealthy, `degraded` if any non-critical check is unhealthy/degraded, and `healthy` otherwise.
**Verified by:** `health-checker.test.ts` › "rejects duplicate check registration", "throws UNKNOWN_CHECK for unregistered name", "aggregates healthy when all critical checks pass", "degrades when non-critical check fails", "aggregates degraded when a check is degraded".
### Requirement: HealthChecker accepts injectable Clock + TimersLike + LoggerLike
The `HealthChecker` constructor SHALL accept optional `clock: Clock`, `timers: TimersLike`, and `logger: LoggerLike`. Wall-clock readings (`responseTime`, `uptimeMs`, ISO `timestamp`, cache expiry) MUST come from the injected clock. Per-check timeouts MUST be scheduled via `timers.setTimer` / `timers.clearTimer`. When a check fails, the logger's `warn` method MUST be invoked with `{ name, message }`.
**Verified by:** `health-checker.test.ts` › "records success result + emits CHECK_PASSED" (uses injected clock), "enforces timeout via TimersLike", "records failure result + emits CHECK_FAILED + logs warn".
### Requirement: HealthChecker emits typed lifecycle events
`HealthChecker` SHALL expose `events: EventBus<HealthEventMap>` that emits `check:passed` (payload `CheckResult`) and `check:failed` (payload `CheckResult`) on every check execution, and `status:changed` (payload `{ previous, current }`) when the aggregate status flips.
**Verified by:** `health-checker.test.ts` › "records success result + emits CHECK_PASSED", "records failure result + emits CHECK_FAILED + logs warn", "emits STATUS_CHANGED on transition".
### Requirement: HealthChecker caches results when cacheTtlMs > 0
When `cacheTtlMs > 0`, a successful or failed `CheckResult` MUST be cached for the TTL and subsequent calls MUST return a copy with `cached: true`. Cache entries MUST expire on the next `runCheck` once the injected clock advances past `expiresAt`. `clearCache()` MUST drop every cached entry. `removeCheck(name)` MUST also drop the cached entry for that name.
**Verified by:** `health-checker.test.ts` › "returns cached result on second call", "clearCache forces re-run", "removeCheck clears cache + returns true".
### Requirement: HEALTH_CHECK_CODES registry via defineCodes
The package MUST publish a stable `HEALTH_CHECK_CODES` registry built with `defineCodes("HEALTH_CHECK", [...])` and an accompanying `HealthCheckError extends SuaveplanError<HealthCheckCode>` class with a public constructor delegating to `super`. Every public option-validation failure MUST throw `HealthCheckError(INVALID_OPTIONS)`. Duplicate / unknown registration paths MUST throw `DUPLICATE_CHECK` / `UNKNOWN_CHECK`. Timeout failures MUST surface as `details.timedOut === true`.
**Verified by:** `error-codes.test.ts` › "namespaces every code under HEALTH_CHECK.", "exposes the expected leaf names", "carries the supplied code, message, cause, and context"; `health-checker.test.ts` › "rejects empty service name", "rejects invalid defaultTimeout", "rejects invalid addCheck arguments".
### Requirement: createHttpCheck accepts injectable fetchImpl + clock
`createHttpCheck` SHALL accept `HttpCheckConfig` carrying optional `fetchImpl: typeof fetch` and an optional `{ clock }` companion. Production code MAY rely on the `globalThis.fetch` default; tests MUST be able to inject a stub. Network errors, abort errors, and unexpected status codes MUST be returned as `unhealthy` `CheckResult` values; the function MUST NOT throw at runtime for transport failures.
**Verified by:** `http-check.test.ts` › "returns healthy on expected status", "treats unexpected status as unhealthy", "respects expectedStatusCodes override", "classifies network errors as unhealthy", "classifies abort errors as timeouts", "falls back to non-Error stringification".
### Requirement: createTcpCheck accepts injectable TcpConnectFn + clock
`createTcpCheck` SHALL accept `TcpCheckConfig` carrying optional `connectImpl: TcpConnectFn` and an optional `{ clock }` companion. The default `connectImpl` MUST wrap `node:net.connect`. The check MUST resolve (never reject) and MUST surface timeouts via `details.timedOut === true`. Late events emitted after the promise settles MUST be ignored.
**Verified by:** `tcp-check.test.ts` › "returns healthy when socket connects", "returns unhealthy on socket error", "returns unhealthy on timeout", "ignores late events once settled", "uses the default node:net connector when not injected".
### Requirement: ResponseBuilder maps statuses to HTTP codes
`ResponseBuilder` SHALL expose `health()`, `ready()`, `live()`. `health()` MUST return statusCode `200` for `healthy` and `degraded` and `503` for `unhealthy`. `ready()` MUST return `200` when `ready === true` and `503` otherwise. `live()` MUST always return `200`.
**Verified by:** `response-builder.test.ts` › "returns 200 for healthy /health", "returns 200 for degraded /health", "returns 503 for unhealthy /health", "returns 200 for ready /ready", "returns 503 for not-ready /ready", "returns 200 for /live".
### Requirement: Telemetry instruments emit on every public call
The package MUST register an OTel meter at `@suaveplan/health-check` exposing:
- `health_check_runs_total` counter — incremented on every executed dependency check, tagged with `name` + `status`.
- `health_check_aggregate_status_total` counter — incremented on every `HealthChecker.health()`, tagged with the aggregate status.
- `health_check_duration_ms` histogram — recorded on every executed dependency check, tagged with `name`.
A real in-memory `MeterProvider` registered before module import MUST see metric points after public calls.
**Verified by:** `telemetry.test.ts` › "records check runs", "records aggregate status", "records check duration".