@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/LICENSE +201 -0
- package/README.md +658 -0
- package/dist/index.d.mts +379 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +367 -0
- package/dist/index.mjs +328 -0
- package/package.json +32 -0
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
|
+
};
|