6.2 KiB
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_totalcounter — incremented on every executed dependency check, tagged withname+status.health_check_aggregate_status_totalcounter — incremented on everyHealthChecker.health(), tagged with the aggregate status.health_check_duration_mshistogram — recorded on every executed dependency check, tagged withname.
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".