first commit
This commit is contained in:
67
docs/observability/monitor/health-check/README.md
Normal file
67
docs/observability/monitor/health-check/README.md
Normal 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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user