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

6.2 KiB
Raw Blame History

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".