@sebspark/health-check 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,367 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ HealthMonitor: () => HealthMonitor,
34
+ TimeoutError: () => TimeoutError,
35
+ UnknownError: () => UnknownError
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/health-monitor.ts
40
+ var import_express = require("express");
41
+
42
+ // src/static-checks.ts
43
+ var import_node_os = __toESM(require("os"));
44
+ var ping = () => ({ status: "ok" });
45
+ var liveness = () => {
46
+ const cpus = import_node_os.default.cpus();
47
+ const total = import_node_os.default.totalmem();
48
+ const free = import_node_os.default.freemem();
49
+ return {
50
+ status: "ok",
51
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
52
+ system: {
53
+ hostname: import_node_os.default.hostname(),
54
+ platform: import_node_os.default.platform(),
55
+ release: import_node_os.default.release(),
56
+ arch: import_node_os.default.arch(),
57
+ uptime: import_node_os.default.uptime(),
58
+ loadavg: import_node_os.default.loadavg(),
59
+ totalmem: total,
60
+ freemem: free,
61
+ memUsedRatio: total > 0 ? (total - free) / total : 0,
62
+ cpus: {
63
+ count: cpus.length,
64
+ model: cpus[0]?.model,
65
+ speedMHz: cpus[0]?.speed
66
+ }
67
+ },
68
+ process: {
69
+ pid: process.pid,
70
+ node: process.versions.node,
71
+ uptime: process.uptime(),
72
+ memory: process.memoryUsage()
73
+ // rss, heapTotal, heapUsed, external, arrayBuffers
74
+ }
75
+ };
76
+ };
77
+
78
+ // src/types.ts
79
+ var TimeoutError = class extends Error {
80
+ constructor() {
81
+ super("timeout");
82
+ this.name = "TimeoutError";
83
+ }
84
+ };
85
+ var UnknownError = class extends Error {
86
+ constructor() {
87
+ super("unknown");
88
+ this.name = "UnknownError";
89
+ }
90
+ };
91
+
92
+ // src/timing.ts
93
+ var throttle = (fn, ms) => {
94
+ let current = null;
95
+ let clearHandle = null;
96
+ const wrapped = (...args) => {
97
+ if (!current) {
98
+ current = fn(...args);
99
+ if (clearHandle) {
100
+ clearTimeout(clearHandle);
101
+ clearHandle = null;
102
+ }
103
+ current.finally(() => {
104
+ if (ms > 0) {
105
+ clearHandle = setTimeout(() => {
106
+ current = null;
107
+ clearHandle = null;
108
+ }, ms);
109
+ } else {
110
+ current = null;
111
+ }
112
+ });
113
+ }
114
+ return current;
115
+ };
116
+ return wrapped;
117
+ };
118
+
119
+ // src/health-monitor.ts
120
+ var HealthMonitor = class {
121
+ _router;
122
+ dependencies;
123
+ isDisposed = false;
124
+ /**
125
+ * Create a new HealthMonitor instance with its own router and dependency map.
126
+ */
127
+ constructor(config) {
128
+ this._router = this.createRouter();
129
+ this.dependencies = /* @__PURE__ */ new Map();
130
+ const thr = config ? config.throttle : 10;
131
+ if (thr > 0) {
132
+ this.ready = throttle(this.ready.bind(this), thr);
133
+ }
134
+ }
135
+ /**
136
+ * Register a named dependency to be checked as part of readiness.
137
+ *
138
+ * @param name - Identifier for the dependency (e.g. "postgres", "redis").
139
+ * @param dependency - DependencyMonitor
140
+ * @returns this for chaining
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * monitor.addDependency('redis', new DependencyMonitor({
145
+ * impact: 'critical',
146
+ * pollRate: 5000,
147
+ * syncCall: async () => redis.ping() ? 'ok' : 'error'
148
+ * }))
149
+ * ```
150
+ */
151
+ addDependency(name, dependency) {
152
+ this.dependencies.set(name, dependency);
153
+ return this;
154
+ }
155
+ /**
156
+ * Internal: create the Express router with /health routes.
157
+ */
158
+ createRouter() {
159
+ const router = (0, import_express.Router)();
160
+ router.get("/health", async (_req, res, next) => {
161
+ try {
162
+ const health = await this.health();
163
+ res.status(200).json(health);
164
+ } catch (err) {
165
+ next(err);
166
+ }
167
+ });
168
+ router.get("/health/ping", (_req, res, next) => {
169
+ try {
170
+ res.status(200).json(this.ping());
171
+ } catch (err) {
172
+ next(err);
173
+ }
174
+ });
175
+ router.get("/health/live", (_req, res, next) => {
176
+ try {
177
+ res.status(200).json(this.live());
178
+ } catch (err) {
179
+ next(err);
180
+ }
181
+ });
182
+ router.get("/health/ready", async (_req, res, next) => {
183
+ try {
184
+ const readiness = await this.ready();
185
+ res.status(200).json(readiness);
186
+ } catch (err) {
187
+ next(err);
188
+ }
189
+ });
190
+ const errorHandler = (err, _req, res, _next) => {
191
+ const message = err instanceof Error ? err.message : String(err);
192
+ res.status(500).json({
193
+ status: "error",
194
+ error: { message }
195
+ });
196
+ };
197
+ router.use(errorHandler);
198
+ return router;
199
+ }
200
+ /**
201
+ * Static ping check — always returns `{ status: "ok" }`.
202
+ */
203
+ ping() {
204
+ return ping();
205
+ }
206
+ /**
207
+ * Liveness probe: reports uptime and current timestamp.
208
+ */
209
+ live() {
210
+ return liveness();
211
+ }
212
+ /**
213
+ * Readiness probe: runs all registered dependencies in parallel
214
+ * and aggregates their results into an overall status.
215
+ *
216
+ * - Any **critical error** → `"error"`
217
+ * - Else any **critical degraded** or any non-critical degraded/error → `"degraded"`
218
+ * - Else → `"ok"`
219
+ *
220
+ * @returns Object with overall `status` and per-dependency `checks`
221
+ *
222
+ * @example
223
+ * ```json
224
+ * {
225
+ * "status": "ok",
226
+ * "checks": {
227
+ * "postgres": {
228
+ * "status": "ok",
229
+ * "impact": "critical",
230
+ * "mode": "inline",
231
+ * "freshness": {
232
+ * "lastChecked": "2025-08-19T12:00:00Z",
233
+ * "lastSuccess": "2025-08-19T12:00:00Z"
234
+ * },
235
+ * "observed": { "latencyMs": 12 }
236
+ * }
237
+ * }
238
+ * }
239
+ * ```
240
+ */
241
+ async ready() {
242
+ const entries = Array.from(this.dependencies.entries());
243
+ const settled = await Promise.allSettled(
244
+ entries.map(async ([name, monitor]) => {
245
+ const check = await monitor.check();
246
+ return [name, check];
247
+ })
248
+ );
249
+ const checks = {};
250
+ for (let i = 0; i < settled.length; i++) {
251
+ const s = settled[i];
252
+ const [name] = entries[i];
253
+ if (s.status === "fulfilled") {
254
+ const [, check] = s.value;
255
+ checks[name] = check;
256
+ } else {
257
+ checks[name] = {
258
+ status: "error",
259
+ impact: this.dependencies.get(name)?.impact,
260
+ mode: "polled",
261
+ freshness: {
262
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
263
+ lastSuccess: null
264
+ },
265
+ observed: void 0,
266
+ error: {
267
+ code: "CHECK_FAILED",
268
+ message: String(s.reason ?? "unknown error")
269
+ }
270
+ };
271
+ }
272
+ }
273
+ let criticalOk = 0;
274
+ let criticalFailing = 0;
275
+ let nonCritOk = 0;
276
+ let nonCritDegraded = 0;
277
+ let nonCritFailing = 0;
278
+ const degradedReasons = [];
279
+ for (const [name, c] of Object.entries(checks)) {
280
+ const isCritical = c.impact === "critical";
281
+ if (c.status === "ok") {
282
+ if (isCritical) criticalOk++;
283
+ else nonCritOk++;
284
+ } else if (c.status === "degraded") {
285
+ if (isCritical)
286
+ criticalFailing++;
287
+ else nonCritDegraded++;
288
+ degradedReasons.push(`${name}:degraded`);
289
+ } else {
290
+ if (isCritical) criticalFailing++;
291
+ else nonCritFailing++;
292
+ degradedReasons.push(`${name}:${c.error?.code ?? "error"}`);
293
+ }
294
+ }
295
+ const summary = {
296
+ critical: { ok: criticalOk, failing: criticalFailing },
297
+ nonCritical: {
298
+ ok: nonCritOk,
299
+ degraded: nonCritDegraded,
300
+ failing: nonCritFailing
301
+ },
302
+ degradedReasons
303
+ };
304
+ let status = "ok";
305
+ const values = Object.values(checks);
306
+ const anyCriticalError = values.some(
307
+ (c) => c.impact === "critical" && c.status === "error"
308
+ );
309
+ const anyDegradedOrError = values.some(
310
+ (c) => c.status === "degraded" || c.status === "error"
311
+ );
312
+ const anyCriticalDegraded = values.some(
313
+ (c) => c.impact === "critical" && c.status === "degraded"
314
+ );
315
+ if (anyCriticalError) status = "error";
316
+ else if (anyCriticalDegraded || anyDegradedOrError) status = "degraded";
317
+ const payload = {
318
+ status,
319
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
320
+ // service: { name, version, instanceId } // (optional; if you add config later)
321
+ summary,
322
+ checks
323
+ };
324
+ return payload;
325
+ }
326
+ async health() {
327
+ const { status, checks, summary } = await this.ready();
328
+ const { timestamp, system, process: process2 } = this.live();
329
+ return {
330
+ status,
331
+ timestamp,
332
+ system,
333
+ process: process2,
334
+ checks,
335
+ summary
336
+ };
337
+ }
338
+ /**
339
+ * Accessor for the Express router that exposes /health endpoints.
340
+ */
341
+ get router() {
342
+ return this._router;
343
+ }
344
+ /**
345
+ * Symbol-based disposer for use with `using`.
346
+ */
347
+ [Symbol.dispose]() {
348
+ this.dispose();
349
+ }
350
+ /**
351
+ * Dispose of the monitor:
352
+ * - marks as disposed
353
+ * - (future: could also stop all dependency monitors)
354
+ */
355
+ dispose() {
356
+ this.isDisposed = true;
357
+ for (const dependency of this.dependencies.values()) {
358
+ dependency.dispose();
359
+ }
360
+ }
361
+ };
362
+ // Annotate the CommonJS export names for ESM import in node:
363
+ 0 && (module.exports = {
364
+ HealthMonitor,
365
+ TimeoutError,
366
+ UnknownError
367
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,328 @@
1
+ // src/health-monitor.ts
2
+ import { Router } from "express";
3
+
4
+ // src/static-checks.ts
5
+ import os from "os";
6
+ var ping = () => ({ status: "ok" });
7
+ var liveness = () => {
8
+ const cpus = os.cpus();
9
+ const total = os.totalmem();
10
+ const free = os.freemem();
11
+ return {
12
+ status: "ok",
13
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14
+ system: {
15
+ hostname: os.hostname(),
16
+ platform: os.platform(),
17
+ release: os.release(),
18
+ arch: os.arch(),
19
+ uptime: os.uptime(),
20
+ loadavg: os.loadavg(),
21
+ totalmem: total,
22
+ freemem: free,
23
+ memUsedRatio: total > 0 ? (total - free) / total : 0,
24
+ cpus: {
25
+ count: cpus.length,
26
+ model: cpus[0]?.model,
27
+ speedMHz: cpus[0]?.speed
28
+ }
29
+ },
30
+ process: {
31
+ pid: process.pid,
32
+ node: process.versions.node,
33
+ uptime: process.uptime(),
34
+ memory: process.memoryUsage()
35
+ // rss, heapTotal, heapUsed, external, arrayBuffers
36
+ }
37
+ };
38
+ };
39
+
40
+ // src/types.ts
41
+ var TimeoutError = class extends Error {
42
+ constructor() {
43
+ super("timeout");
44
+ this.name = "TimeoutError";
45
+ }
46
+ };
47
+ var UnknownError = class extends Error {
48
+ constructor() {
49
+ super("unknown");
50
+ this.name = "UnknownError";
51
+ }
52
+ };
53
+
54
+ // src/timing.ts
55
+ var throttle = (fn, ms) => {
56
+ let current = null;
57
+ let clearHandle = null;
58
+ const wrapped = (...args) => {
59
+ if (!current) {
60
+ current = fn(...args);
61
+ if (clearHandle) {
62
+ clearTimeout(clearHandle);
63
+ clearHandle = null;
64
+ }
65
+ current.finally(() => {
66
+ if (ms > 0) {
67
+ clearHandle = setTimeout(() => {
68
+ current = null;
69
+ clearHandle = null;
70
+ }, ms);
71
+ } else {
72
+ current = null;
73
+ }
74
+ });
75
+ }
76
+ return current;
77
+ };
78
+ return wrapped;
79
+ };
80
+
81
+ // src/health-monitor.ts
82
+ var HealthMonitor = class {
83
+ _router;
84
+ dependencies;
85
+ isDisposed = false;
86
+ /**
87
+ * Create a new HealthMonitor instance with its own router and dependency map.
88
+ */
89
+ constructor(config) {
90
+ this._router = this.createRouter();
91
+ this.dependencies = /* @__PURE__ */ new Map();
92
+ const thr = config ? config.throttle : 10;
93
+ if (thr > 0) {
94
+ this.ready = throttle(this.ready.bind(this), thr);
95
+ }
96
+ }
97
+ /**
98
+ * Register a named dependency to be checked as part of readiness.
99
+ *
100
+ * @param name - Identifier for the dependency (e.g. "postgres", "redis").
101
+ * @param dependency - DependencyMonitor
102
+ * @returns this for chaining
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * monitor.addDependency('redis', new DependencyMonitor({
107
+ * impact: 'critical',
108
+ * pollRate: 5000,
109
+ * syncCall: async () => redis.ping() ? 'ok' : 'error'
110
+ * }))
111
+ * ```
112
+ */
113
+ addDependency(name, dependency) {
114
+ this.dependencies.set(name, dependency);
115
+ return this;
116
+ }
117
+ /**
118
+ * Internal: create the Express router with /health routes.
119
+ */
120
+ createRouter() {
121
+ const router = Router();
122
+ router.get("/health", async (_req, res, next) => {
123
+ try {
124
+ const health = await this.health();
125
+ res.status(200).json(health);
126
+ } catch (err) {
127
+ next(err);
128
+ }
129
+ });
130
+ router.get("/health/ping", (_req, res, next) => {
131
+ try {
132
+ res.status(200).json(this.ping());
133
+ } catch (err) {
134
+ next(err);
135
+ }
136
+ });
137
+ router.get("/health/live", (_req, res, next) => {
138
+ try {
139
+ res.status(200).json(this.live());
140
+ } catch (err) {
141
+ next(err);
142
+ }
143
+ });
144
+ router.get("/health/ready", async (_req, res, next) => {
145
+ try {
146
+ const readiness = await this.ready();
147
+ res.status(200).json(readiness);
148
+ } catch (err) {
149
+ next(err);
150
+ }
151
+ });
152
+ const errorHandler = (err, _req, res, _next) => {
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ res.status(500).json({
155
+ status: "error",
156
+ error: { message }
157
+ });
158
+ };
159
+ router.use(errorHandler);
160
+ return router;
161
+ }
162
+ /**
163
+ * Static ping check — always returns `{ status: "ok" }`.
164
+ */
165
+ ping() {
166
+ return ping();
167
+ }
168
+ /**
169
+ * Liveness probe: reports uptime and current timestamp.
170
+ */
171
+ live() {
172
+ return liveness();
173
+ }
174
+ /**
175
+ * Readiness probe: runs all registered dependencies in parallel
176
+ * and aggregates their results into an overall status.
177
+ *
178
+ * - Any **critical error** → `"error"`
179
+ * - Else any **critical degraded** or any non-critical degraded/error → `"degraded"`
180
+ * - Else → `"ok"`
181
+ *
182
+ * @returns Object with overall `status` and per-dependency `checks`
183
+ *
184
+ * @example
185
+ * ```json
186
+ * {
187
+ * "status": "ok",
188
+ * "checks": {
189
+ * "postgres": {
190
+ * "status": "ok",
191
+ * "impact": "critical",
192
+ * "mode": "inline",
193
+ * "freshness": {
194
+ * "lastChecked": "2025-08-19T12:00:00Z",
195
+ * "lastSuccess": "2025-08-19T12:00:00Z"
196
+ * },
197
+ * "observed": { "latencyMs": 12 }
198
+ * }
199
+ * }
200
+ * }
201
+ * ```
202
+ */
203
+ async ready() {
204
+ const entries = Array.from(this.dependencies.entries());
205
+ const settled = await Promise.allSettled(
206
+ entries.map(async ([name, monitor]) => {
207
+ const check = await monitor.check();
208
+ return [name, check];
209
+ })
210
+ );
211
+ const checks = {};
212
+ for (let i = 0; i < settled.length; i++) {
213
+ const s = settled[i];
214
+ const [name] = entries[i];
215
+ if (s.status === "fulfilled") {
216
+ const [, check] = s.value;
217
+ checks[name] = check;
218
+ } else {
219
+ checks[name] = {
220
+ status: "error",
221
+ impact: this.dependencies.get(name)?.impact,
222
+ mode: "polled",
223
+ freshness: {
224
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
225
+ lastSuccess: null
226
+ },
227
+ observed: void 0,
228
+ error: {
229
+ code: "CHECK_FAILED",
230
+ message: String(s.reason ?? "unknown error")
231
+ }
232
+ };
233
+ }
234
+ }
235
+ let criticalOk = 0;
236
+ let criticalFailing = 0;
237
+ let nonCritOk = 0;
238
+ let nonCritDegraded = 0;
239
+ let nonCritFailing = 0;
240
+ const degradedReasons = [];
241
+ for (const [name, c] of Object.entries(checks)) {
242
+ const isCritical = c.impact === "critical";
243
+ if (c.status === "ok") {
244
+ if (isCritical) criticalOk++;
245
+ else nonCritOk++;
246
+ } else if (c.status === "degraded") {
247
+ if (isCritical)
248
+ criticalFailing++;
249
+ else nonCritDegraded++;
250
+ degradedReasons.push(`${name}:degraded`);
251
+ } else {
252
+ if (isCritical) criticalFailing++;
253
+ else nonCritFailing++;
254
+ degradedReasons.push(`${name}:${c.error?.code ?? "error"}`);
255
+ }
256
+ }
257
+ const summary = {
258
+ critical: { ok: criticalOk, failing: criticalFailing },
259
+ nonCritical: {
260
+ ok: nonCritOk,
261
+ degraded: nonCritDegraded,
262
+ failing: nonCritFailing
263
+ },
264
+ degradedReasons
265
+ };
266
+ let status = "ok";
267
+ const values = Object.values(checks);
268
+ const anyCriticalError = values.some(
269
+ (c) => c.impact === "critical" && c.status === "error"
270
+ );
271
+ const anyDegradedOrError = values.some(
272
+ (c) => c.status === "degraded" || c.status === "error"
273
+ );
274
+ const anyCriticalDegraded = values.some(
275
+ (c) => c.impact === "critical" && c.status === "degraded"
276
+ );
277
+ if (anyCriticalError) status = "error";
278
+ else if (anyCriticalDegraded || anyDegradedOrError) status = "degraded";
279
+ const payload = {
280
+ status,
281
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
282
+ // service: { name, version, instanceId } // (optional; if you add config later)
283
+ summary,
284
+ checks
285
+ };
286
+ return payload;
287
+ }
288
+ async health() {
289
+ const { status, checks, summary } = await this.ready();
290
+ const { timestamp, system, process: process2 } = this.live();
291
+ return {
292
+ status,
293
+ timestamp,
294
+ system,
295
+ process: process2,
296
+ checks,
297
+ summary
298
+ };
299
+ }
300
+ /**
301
+ * Accessor for the Express router that exposes /health endpoints.
302
+ */
303
+ get router() {
304
+ return this._router;
305
+ }
306
+ /**
307
+ * Symbol-based disposer for use with `using`.
308
+ */
309
+ [Symbol.dispose]() {
310
+ this.dispose();
311
+ }
312
+ /**
313
+ * Dispose of the monitor:
314
+ * - marks as disposed
315
+ * - (future: could also stop all dependency monitors)
316
+ */
317
+ dispose() {
318
+ this.isDisposed = true;
319
+ for (const dependency of this.dependencies.values()) {
320
+ dependency.dispose();
321
+ }
322
+ }
323
+ };
324
+ export {
325
+ HealthMonitor,
326
+ TimeoutError,
327
+ UnknownError
328
+ };