@catbee/utils 2.1.1 → 2.2.1
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/README.md +1 -1
- package/config/index.cjs +5 -6
- package/config/index.mjs +5 -6
- package/healthz-server/index.cjs +64 -14
- package/healthz-server/index.d.ts +65 -31
- package/healthz-server/index.mjs +64 -14
- package/package.json +1 -1
- package/server/index.cjs +244 -137
- package/server/index.d.ts +70 -32
- package/server/index.mjs +247 -140
- package/types/index.d.ts +142 -47
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ const config = new ServerConfigBuilder()
|
|
|
55
55
|
.withCors({ origin: '*' })
|
|
56
56
|
.enableRateLimit({ max: 50, windowMs: 60000 })
|
|
57
57
|
.enableRequestLogging({ ignorePaths: ['/healthz', '/metrics'] })
|
|
58
|
-
.
|
|
58
|
+
.enableHealthzServer({ port: 8282 })
|
|
59
59
|
.enableOpenApi('./openapi.yaml', { mountPath: '/docs' })
|
|
60
60
|
.withGlobalHeaders({ 'X-Powered-By': 'Catbee' })
|
|
61
61
|
.withGlobalPrefix('/api')
|
package/config/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
var env = require('@catbee/utils/env');
|
|
28
28
|
var id = require('@catbee/utils/id');
|
|
29
29
|
var object = require('@catbee/utils/object');
|
|
30
|
+
var healthzServer = require('@catbee/utils/healthz-server');
|
|
30
31
|
|
|
31
32
|
var __defProp = Object.defineProperty;
|
|
32
33
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -78,10 +79,9 @@ var defaultServerConfig = {
|
|
|
78
79
|
verbose: env.Env.getBoolean("SERVER_OPENAPI_VERBOSE", false),
|
|
79
80
|
withGlobalPrefix: env.Env.getBoolean("SERVER_OPENAPI_WITH_GLOBAL_PREFIX", false)
|
|
80
81
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
withGlobalPrefix: env.Env.getBoolean("SERVER_HEALTH_CHECK_WITH_GLOBAL_PREFIX", false)
|
|
82
|
+
healthzServer: {
|
|
83
|
+
enable: env.Env.getBoolean("SERVER_HEALTHZ_ENABLE", env.Env.getBoolean("HEALTHZ_ENABLE", false)),
|
|
84
|
+
...healthzServer.getDefaultHealthzConfig()
|
|
85
85
|
},
|
|
86
86
|
requestTimeout: env.Env.getDuration("SERVER_REQUEST_TIMEOUT_MS", 0),
|
|
87
87
|
responseTime: {
|
|
@@ -98,8 +98,7 @@ var defaultServerConfig = {
|
|
|
98
98
|
enable: env.Env.getBoolean("SERVER_SERVICE_VERSION_ENABLE", false),
|
|
99
99
|
headerName: env.Env.get("SERVER_SERVICE_VERSION_HEADER_NAME", "x-service-version"),
|
|
100
100
|
version: env.Env.get("${npm_package_version}", "0.0.0")
|
|
101
|
-
}
|
|
102
|
-
skipHealthzChecksValidation: env.Env.getBoolean("SERVER_SKIP_HEALTHZ_CHECKS_VALIDATION", false)
|
|
101
|
+
}
|
|
103
102
|
};
|
|
104
103
|
var defaultCatbeeConfig = {
|
|
105
104
|
logger: {
|
package/config/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { Env } from '@catbee/utils/env';
|
|
26
26
|
import { uuid } from '@catbee/utils/id';
|
|
27
27
|
import { deepClone, deepObjMerge } from '@catbee/utils/object';
|
|
28
|
+
import { getDefaultHealthzConfig } from '@catbee/utils/healthz-server';
|
|
28
29
|
|
|
29
30
|
var __defProp = Object.defineProperty;
|
|
30
31
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -76,10 +77,9 @@ var defaultServerConfig = {
|
|
|
76
77
|
verbose: Env.getBoolean("SERVER_OPENAPI_VERBOSE", false),
|
|
77
78
|
withGlobalPrefix: Env.getBoolean("SERVER_OPENAPI_WITH_GLOBAL_PREFIX", false)
|
|
78
79
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
withGlobalPrefix: Env.getBoolean("SERVER_HEALTH_CHECK_WITH_GLOBAL_PREFIX", false)
|
|
80
|
+
healthzServer: {
|
|
81
|
+
enable: Env.getBoolean("SERVER_HEALTHZ_ENABLE", Env.getBoolean("HEALTHZ_ENABLE", false)),
|
|
82
|
+
...getDefaultHealthzConfig()
|
|
83
83
|
},
|
|
84
84
|
requestTimeout: Env.getDuration("SERVER_REQUEST_TIMEOUT_MS", 0),
|
|
85
85
|
responseTime: {
|
|
@@ -96,8 +96,7 @@ var defaultServerConfig = {
|
|
|
96
96
|
enable: Env.getBoolean("SERVER_SERVICE_VERSION_ENABLE", false),
|
|
97
97
|
headerName: Env.get("SERVER_SERVICE_VERSION_HEADER_NAME", "x-service-version"),
|
|
98
98
|
version: Env.get("${npm_package_version}", "0.0.0")
|
|
99
|
-
}
|
|
100
|
-
skipHealthzChecksValidation: Env.getBoolean("SERVER_SKIP_HEALTHZ_CHECKS_VALIDATION", false)
|
|
99
|
+
}
|
|
101
100
|
};
|
|
102
101
|
var defaultCatbeeConfig = {
|
|
103
102
|
logger: {
|
package/healthz-server/index.cjs
CHANGED
|
@@ -36,10 +36,11 @@ function getDefaultHealthzConfig() {
|
|
|
36
36
|
healthzPath: env.Env.get("HEALTHZ_PATH", "") || env.Env.get("SERVER_HEALTHZ_PATH", "") || env.Env.get("SERVER_HEALTH_CHECK_PATH", "/healthz"),
|
|
37
37
|
readyzPath: env.Env.get("HEALTHZ_READYZ_PATH", "") || env.Env.get("SERVER_READYZ_PATH", "/readyz"),
|
|
38
38
|
startupzPath: env.Env.get("HEALTHZ_STARTUPZ_PATH", "") || env.Env.get("SERVER_STARTUPZ_PATH", "/startupz"),
|
|
39
|
-
detailed: env.Env.getBoolean("HEALTHZ_DETAILED", env.Env.getBoolean("SERVER_HEALTH_CHECK_DETAILED_OUTPUT", true)),
|
|
39
|
+
detailed: env.Env.getBoolean("HEALTHZ_DETAILED", env.Env.getBoolean("SERVER_HEALTHZ_DETAILED", env.Env.getBoolean("SERVER_HEALTH_CHECK_DETAILED_OUTPUT", true))),
|
|
40
40
|
checks: [],
|
|
41
|
-
checkTimeoutMs: env.Env.getDuration("HEALTHZ_CHECK_TIMEOUT_MS", 5e3),
|
|
42
|
-
shutdownDelayMs: env.Env.getDuration("HEALTHZ_SHUTDOWN_DELAY_MS", 5e3)
|
|
41
|
+
checkTimeoutMs: env.Env.getDuration("HEALTHZ_CHECK_TIMEOUT_MS", env.Env.getDuration("SERVER_HEALTHZ_CHECK_TIMEOUT_MS", 5e3)),
|
|
42
|
+
shutdownDelayMs: env.Env.getDuration("HEALTHZ_SHUTDOWN_DELAY_MS", env.Env.getDuration("SERVER_HEALTHZ_SHUTDOWN_DELAY_MS", 5e3)),
|
|
43
|
+
handleSignals: true
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
__name(getDefaultHealthzConfig, "getDefaultHealthzConfig");
|
|
@@ -66,8 +67,10 @@ var HealthzServer = class _HealthzServer {
|
|
|
66
67
|
server;
|
|
67
68
|
config;
|
|
68
69
|
startedAt = Date.now();
|
|
69
|
-
/** Whether the health server
|
|
70
|
-
|
|
70
|
+
/** Whether the health HTTP server is currently running and listening on its port */
|
|
71
|
+
running = false;
|
|
72
|
+
/** Whether application startup has completed (for `/startupz`) */
|
|
73
|
+
startupComplete = false;
|
|
71
74
|
/** Whether the service is ready to receive traffic (for `/readyz`) */
|
|
72
75
|
ready = false;
|
|
73
76
|
shuttingDown = false;
|
|
@@ -76,8 +79,16 @@ var HealthzServer = class _HealthzServer {
|
|
|
76
79
|
constructor(config) {
|
|
77
80
|
this.config = config;
|
|
78
81
|
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if (this.config.handleSignals !== false) {
|
|
83
|
+
process.once("SIGTERM", this.onSigterm);
|
|
84
|
+
process.once("SIGINT", this.onSigint);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns default Healthz server configuration resolved from environment variables.
|
|
89
|
+
*/
|
|
90
|
+
static getDefaultConfig() {
|
|
91
|
+
return getDefaultHealthzConfig();
|
|
81
92
|
}
|
|
82
93
|
/**
|
|
83
94
|
* Boot the health-check server.
|
|
@@ -100,7 +111,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
100
111
|
port: config.port
|
|
101
112
|
}, () => {
|
|
102
113
|
instance.server.off("error", onError);
|
|
103
|
-
instance.
|
|
114
|
+
instance.running = true;
|
|
104
115
|
const addr = instance.server.address();
|
|
105
116
|
if (!addr || typeof addr === "string") {
|
|
106
117
|
instance.cleanup();
|
|
@@ -116,9 +127,19 @@ var HealthzServer = class _HealthzServer {
|
|
|
116
127
|
});
|
|
117
128
|
});
|
|
118
129
|
}
|
|
119
|
-
/** Whether the
|
|
120
|
-
static
|
|
121
|
-
return _global[SINGLETON_KEY]?.
|
|
130
|
+
/** Whether the Healthz HTTP probe server is currently running and listening on its port. */
|
|
131
|
+
static isRunning() {
|
|
132
|
+
return _global[SINGLETON_KEY]?.running ?? false;
|
|
133
|
+
}
|
|
134
|
+
/** Mark application startup as completed (switches `/startupz` to 200). */
|
|
135
|
+
static markStartupComplete() {
|
|
136
|
+
const instance = _global[SINGLETON_KEY];
|
|
137
|
+
if (!instance) return;
|
|
138
|
+
instance.startupComplete = true;
|
|
139
|
+
}
|
|
140
|
+
/** Whether application startup has completed (for `/startupz`). */
|
|
141
|
+
static isStartupComplete() {
|
|
142
|
+
return _global[SINGLETON_KEY]?.startupComplete ?? false;
|
|
122
143
|
}
|
|
123
144
|
/** Mark the service as ready / not-ready for traffic. */
|
|
124
145
|
static setReady(ready) {
|
|
@@ -148,6 +169,34 @@ var HealthzServer = class _HealthzServer {
|
|
|
148
169
|
static getInstance() {
|
|
149
170
|
return _global[SINGLETON_KEY];
|
|
150
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Register a named check on this HealthzServer instance dynamically.
|
|
174
|
+
*
|
|
175
|
+
* @param check - The named check to register
|
|
176
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
177
|
+
* @returns This instance for chaining
|
|
178
|
+
*/
|
|
179
|
+
registerCheck(check, type = "readiness") {
|
|
180
|
+
if (type === "liveness" || type === "both") {
|
|
181
|
+
this.config.checks.push(check);
|
|
182
|
+
}
|
|
183
|
+
if (type === "readiness" || type === "both") {
|
|
184
|
+
this.config.readinessChecks ??= [
|
|
185
|
+
...this.config.checks
|
|
186
|
+
];
|
|
187
|
+
this.config.readinessChecks.push(check);
|
|
188
|
+
}
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Register a named check on the active singleton HealthzServer instance (if started).
|
|
193
|
+
*
|
|
194
|
+
* @param check - The named check to register
|
|
195
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
196
|
+
*/
|
|
197
|
+
static registerCheck(check, type = "readiness") {
|
|
198
|
+
_global[SINGLETON_KEY]?.registerCheck(check, type);
|
|
199
|
+
}
|
|
151
200
|
async handleRequest(req, res) {
|
|
152
201
|
const isHead = req.method === "HEAD";
|
|
153
202
|
if (req.method !== "GET" && !isHead) {
|
|
@@ -251,7 +300,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
251
300
|
}
|
|
252
301
|
/** `/startupz` — Startup probe */
|
|
253
302
|
handleStartup(res, isHead = false) {
|
|
254
|
-
if (this.
|
|
303
|
+
if (this.startupComplete) {
|
|
255
304
|
this.sendProbe(res, 200, "ok", [], isHead);
|
|
256
305
|
} else {
|
|
257
306
|
this.sendProbe(res, 503, "unhealthy", [
|
|
@@ -259,7 +308,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
259
308
|
name: "startup",
|
|
260
309
|
ok: false,
|
|
261
310
|
durationMs: 0,
|
|
262
|
-
error: "
|
|
311
|
+
error: "Application startup not complete"
|
|
263
312
|
}
|
|
264
313
|
], isHead);
|
|
265
314
|
}
|
|
@@ -346,7 +395,8 @@ var HealthzServer = class _HealthzServer {
|
|
|
346
395
|
process.off("SIGTERM", this.onSigterm);
|
|
347
396
|
process.off("SIGINT", this.onSigint);
|
|
348
397
|
this.ready = false;
|
|
349
|
-
this.
|
|
398
|
+
this.startupComplete = false;
|
|
399
|
+
this.running = false;
|
|
350
400
|
this.shuttingDown = false;
|
|
351
401
|
}
|
|
352
402
|
};
|
|
@@ -169,6 +169,13 @@ interface CatbeeHealthzServerConfig {
|
|
|
169
169
|
* - **env**: `HEALTHZ_SHUTDOWN_DELAY_MS`
|
|
170
170
|
*/
|
|
171
171
|
shutdownDelayMs?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Whether the HealthzServer should register its own SIGTERM/SIGINT process signal listeners.
|
|
174
|
+
* When managed by an orchestrating server (such as Catbee ExpressServer), set this to `false`
|
|
175
|
+
* to prevent signal listener conflicts and allow coordinated teardown.
|
|
176
|
+
* - **default**: `true`
|
|
177
|
+
*/
|
|
178
|
+
handleSignals?: boolean;
|
|
172
179
|
}
|
|
173
180
|
/**
|
|
174
181
|
* Address information returned after the server starts listening.
|
|
@@ -179,6 +186,29 @@ interface HealthzAddressInfo {
|
|
|
179
186
|
port: number;
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Resolved configuration with all defaults applied.
|
|
191
|
+
* Internal-only — consumers interact with `CatbeeHealthzServerConfig`.
|
|
192
|
+
*
|
|
193
|
+
* `readinessChecks` is kept as `NamedCheck[] | undefined` so that
|
|
194
|
+
* "omitted" (-> fall back to `checks`) is distinguishable from
|
|
195
|
+
* "explicitly empty" (-> run nothing).
|
|
196
|
+
*/
|
|
197
|
+
interface ResolvedHealthzConfig extends Required<Omit<CatbeeHealthzServerConfig, 'onHealthCheck' | 'onReadinessCheck' | 'readinessChecks'>> {
|
|
198
|
+
onHealthCheck?: CatbeeHealthzServerConfig['onHealthCheck'];
|
|
199
|
+
onReadinessCheck?: CatbeeHealthzServerConfig['onReadinessCheck'];
|
|
200
|
+
readinessChecks?: CatbeeHealthzServerConfig['readinessChecks'];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Loads default Healthz server configuration from environment variables.
|
|
204
|
+
*/
|
|
205
|
+
declare function getDefaultHealthzConfig(): ResolvedHealthzConfig;
|
|
206
|
+
/**
|
|
207
|
+
* Merge user-supplied configuration with environment-resolved defaults.
|
|
208
|
+
* Undefined user values do not override resolved defaults.
|
|
209
|
+
*/
|
|
210
|
+
declare function resolveConfig(userConfig?: CatbeeHealthzServerConfig): ResolvedHealthzConfig;
|
|
211
|
+
|
|
182
212
|
/**
|
|
183
213
|
* Standalone HTTP health-check server designed for Kubernetes probes
|
|
184
214
|
* and microservice orchestration.
|
|
@@ -189,7 +219,7 @@ interface HealthzAddressInfo {
|
|
|
189
219
|
* |-----------|-------------|------------------|-----------|
|
|
190
220
|
* | Liveness | `/healthz` | `livenessProbe` | Runs configured checks; 200 = alive, 503 = unhealthy |
|
|
191
221
|
* | Readiness | `/readyz` | `readinessProbe` | Checks readiness flag + readiness checks; 503 while not ready |
|
|
192
|
-
* | Startup | `/startupz` | `startupProbe` | 200 once
|
|
222
|
+
* | Startup | `/startupz` | `startupProbe` | 200 once application startup completes (`markStartupComplete()`); 503 while booting |
|
|
193
223
|
*
|
|
194
224
|
* ### Kubernetes Probe Best Practices:
|
|
195
225
|
* - **Liveness (`/healthz`)**: Keep these checks extremely lightweight (e.g. process is responsive,
|
|
@@ -199,7 +229,8 @@ interface HealthzAddressInfo {
|
|
|
199
229
|
* - **Readiness (`/readyz`)**: Place external dependency checks (DB, Redis, downstream APIs) here via
|
|
200
230
|
* `readinessChecks`. If a dependency fails, Kubernetes temporarily pulls the pod from service endpoints
|
|
201
231
|
* without restarting the container, allowing it to recover gracefully.
|
|
202
|
-
* - **Startup (`/startupz`)**: Verifies the
|
|
232
|
+
* - **Startup (`/startupz`)**: Verifies the application has completed its startup sequence
|
|
233
|
+
* (signaled via `markStartupComplete()`). Protects slow-starting applications from premature liveness kills.
|
|
203
234
|
*
|
|
204
235
|
* @example
|
|
205
236
|
* ```ts
|
|
@@ -207,11 +238,9 @@ interface HealthzAddressInfo {
|
|
|
207
238
|
*
|
|
208
239
|
* const addr = await HealthzServer.start({
|
|
209
240
|
* port: 8282,
|
|
210
|
-
* // Keep liveness lightweight:
|
|
211
241
|
* checks: [
|
|
212
242
|
* { name: 'process', check: () => true },
|
|
213
243
|
* ],
|
|
214
|
-
* // Place dependency checks on readiness:
|
|
215
244
|
* readinessChecks: [
|
|
216
245
|
* { name: 'database', check: (signal) => db.ping({ signal }) },
|
|
217
246
|
* { name: 'redis', check: (signal) => redis.ping({ signal }) },
|
|
@@ -219,6 +248,9 @@ interface HealthzAddressInfo {
|
|
|
219
248
|
* shutdownDelayMs: 10_000,
|
|
220
249
|
* });
|
|
221
250
|
*
|
|
251
|
+
* // Signal application startup complete (switches /startupz to 200):
|
|
252
|
+
* HealthzServer.markStartupComplete();
|
|
253
|
+
*
|
|
222
254
|
* // Signal readiness after all background services and migrations are ready:
|
|
223
255
|
* HealthzServer.setReady(true);
|
|
224
256
|
* ```
|
|
@@ -227,21 +259,31 @@ declare class HealthzServer {
|
|
|
227
259
|
private readonly server;
|
|
228
260
|
private readonly config;
|
|
229
261
|
private readonly startedAt;
|
|
230
|
-
/** Whether the health server
|
|
231
|
-
private
|
|
262
|
+
/** Whether the health HTTP server is currently running and listening on its port */
|
|
263
|
+
private running;
|
|
264
|
+
/** Whether application startup has completed (for `/startupz`) */
|
|
265
|
+
private startupComplete;
|
|
232
266
|
/** Whether the service is ready to receive traffic (for `/readyz`) */
|
|
233
267
|
private ready;
|
|
234
268
|
private shuttingDown;
|
|
235
269
|
private readonly onSigterm;
|
|
236
270
|
private readonly onSigint;
|
|
237
271
|
private constructor();
|
|
272
|
+
/**
|
|
273
|
+
* Returns default Healthz server configuration resolved from environment variables.
|
|
274
|
+
*/
|
|
275
|
+
static getDefaultConfig(): ResolvedHealthzConfig;
|
|
238
276
|
/**
|
|
239
277
|
* Boot the health-check server.
|
|
240
278
|
* Returns `null` if a server is already running in this process.
|
|
241
279
|
*/
|
|
242
280
|
static start(opts?: CatbeeHealthzServerConfig): Promise<HealthzAddressInfo | null>;
|
|
243
|
-
/** Whether the
|
|
244
|
-
static
|
|
281
|
+
/** Whether the Healthz HTTP probe server is currently running and listening on its port. */
|
|
282
|
+
static isRunning(): boolean;
|
|
283
|
+
/** Mark application startup as completed (switches `/startupz` to 200). */
|
|
284
|
+
static markStartupComplete(): void;
|
|
285
|
+
/** Whether application startup has completed (for `/startupz`). */
|
|
286
|
+
static isStartupComplete(): boolean;
|
|
245
287
|
/** Mark the service as ready / not-ready for traffic. */
|
|
246
288
|
static setReady(ready: boolean): void;
|
|
247
289
|
/** Whether the service is currently marked as ready. */
|
|
@@ -249,6 +291,21 @@ declare class HealthzServer {
|
|
|
249
291
|
/** Gracefully stop the health-check server. */
|
|
250
292
|
static stop(): Promise<void>;
|
|
251
293
|
static getInstance(): HealthzServer | undefined;
|
|
294
|
+
/**
|
|
295
|
+
* Register a named check on this HealthzServer instance dynamically.
|
|
296
|
+
*
|
|
297
|
+
* @param check - The named check to register
|
|
298
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
299
|
+
* @returns This instance for chaining
|
|
300
|
+
*/
|
|
301
|
+
registerCheck(check: NamedCheck, type?: 'liveness' | 'readiness' | 'both'): this;
|
|
302
|
+
/**
|
|
303
|
+
* Register a named check on the active singleton HealthzServer instance (if started).
|
|
304
|
+
*
|
|
305
|
+
* @param check - The named check to register
|
|
306
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
307
|
+
*/
|
|
308
|
+
static registerCheck(check: NamedCheck, type?: 'liveness' | 'readiness' | 'both'): void;
|
|
252
309
|
private handleRequest;
|
|
253
310
|
/** `/healthz` — Liveness probe */
|
|
254
311
|
private handleLiveness;
|
|
@@ -264,28 +321,5 @@ declare class HealthzServer {
|
|
|
264
321
|
private cleanup;
|
|
265
322
|
}
|
|
266
323
|
|
|
267
|
-
/**
|
|
268
|
-
* Resolved configuration with all defaults applied.
|
|
269
|
-
* Internal-only — consumers interact with `CatbeeHealthzServerConfig`.
|
|
270
|
-
*
|
|
271
|
-
* `readinessChecks` is kept as `NamedCheck[] | undefined` so that
|
|
272
|
-
* "omitted" (-> fall back to `checks`) is distinguishable from
|
|
273
|
-
* "explicitly empty" (-> run nothing).
|
|
274
|
-
*/
|
|
275
|
-
interface ResolvedHealthzConfig extends Required<Omit<CatbeeHealthzServerConfig, 'onHealthCheck' | 'onReadinessCheck' | 'readinessChecks'>> {
|
|
276
|
-
onHealthCheck?: CatbeeHealthzServerConfig['onHealthCheck'];
|
|
277
|
-
onReadinessCheck?: CatbeeHealthzServerConfig['onReadinessCheck'];
|
|
278
|
-
readinessChecks?: CatbeeHealthzServerConfig['readinessChecks'];
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Loads default Healthz server configuration from environment variables.
|
|
282
|
-
*/
|
|
283
|
-
declare function getDefaultHealthzConfig(): ResolvedHealthzConfig;
|
|
284
|
-
/**
|
|
285
|
-
* Merge user-supplied configuration with environment-resolved defaults.
|
|
286
|
-
* Undefined user values do not override resolved defaults.
|
|
287
|
-
*/
|
|
288
|
-
declare function resolveConfig(userConfig?: CatbeeHealthzServerConfig): ResolvedHealthzConfig;
|
|
289
|
-
|
|
290
324
|
export { HealthzServer, getDefaultHealthzConfig, resolveConfig };
|
|
291
325
|
export type { CatbeeHealthzServerConfig, CheckResult, HealthCheckFn, HealthzAddressInfo, NamedCheck, ProbeResponse, ProbeStatus, ReadinessCheckFn, ResolvedHealthzConfig };
|
package/healthz-server/index.mjs
CHANGED
|
@@ -34,10 +34,11 @@ function getDefaultHealthzConfig() {
|
|
|
34
34
|
healthzPath: Env.get("HEALTHZ_PATH", "") || Env.get("SERVER_HEALTHZ_PATH", "") || Env.get("SERVER_HEALTH_CHECK_PATH", "/healthz"),
|
|
35
35
|
readyzPath: Env.get("HEALTHZ_READYZ_PATH", "") || Env.get("SERVER_READYZ_PATH", "/readyz"),
|
|
36
36
|
startupzPath: Env.get("HEALTHZ_STARTUPZ_PATH", "") || Env.get("SERVER_STARTUPZ_PATH", "/startupz"),
|
|
37
|
-
detailed: Env.getBoolean("HEALTHZ_DETAILED", Env.getBoolean("SERVER_HEALTH_CHECK_DETAILED_OUTPUT", true)),
|
|
37
|
+
detailed: Env.getBoolean("HEALTHZ_DETAILED", Env.getBoolean("SERVER_HEALTHZ_DETAILED", Env.getBoolean("SERVER_HEALTH_CHECK_DETAILED_OUTPUT", true))),
|
|
38
38
|
checks: [],
|
|
39
|
-
checkTimeoutMs: Env.getDuration("HEALTHZ_CHECK_TIMEOUT_MS", 5e3),
|
|
40
|
-
shutdownDelayMs: Env.getDuration("HEALTHZ_SHUTDOWN_DELAY_MS", 5e3)
|
|
39
|
+
checkTimeoutMs: Env.getDuration("HEALTHZ_CHECK_TIMEOUT_MS", Env.getDuration("SERVER_HEALTHZ_CHECK_TIMEOUT_MS", 5e3)),
|
|
40
|
+
shutdownDelayMs: Env.getDuration("HEALTHZ_SHUTDOWN_DELAY_MS", Env.getDuration("SERVER_HEALTHZ_SHUTDOWN_DELAY_MS", 5e3)),
|
|
41
|
+
handleSignals: true
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
__name(getDefaultHealthzConfig, "getDefaultHealthzConfig");
|
|
@@ -64,8 +65,10 @@ var HealthzServer = class _HealthzServer {
|
|
|
64
65
|
server;
|
|
65
66
|
config;
|
|
66
67
|
startedAt = Date.now();
|
|
67
|
-
/** Whether the health server
|
|
68
|
-
|
|
68
|
+
/** Whether the health HTTP server is currently running and listening on its port */
|
|
69
|
+
running = false;
|
|
70
|
+
/** Whether application startup has completed (for `/startupz`) */
|
|
71
|
+
startupComplete = false;
|
|
69
72
|
/** Whether the service is ready to receive traffic (for `/readyz`) */
|
|
70
73
|
ready = false;
|
|
71
74
|
shuttingDown = false;
|
|
@@ -74,8 +77,16 @@ var HealthzServer = class _HealthzServer {
|
|
|
74
77
|
constructor(config) {
|
|
75
78
|
this.config = config;
|
|
76
79
|
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
if (this.config.handleSignals !== false) {
|
|
81
|
+
process.once("SIGTERM", this.onSigterm);
|
|
82
|
+
process.once("SIGINT", this.onSigint);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns default Healthz server configuration resolved from environment variables.
|
|
87
|
+
*/
|
|
88
|
+
static getDefaultConfig() {
|
|
89
|
+
return getDefaultHealthzConfig();
|
|
79
90
|
}
|
|
80
91
|
/**
|
|
81
92
|
* Boot the health-check server.
|
|
@@ -98,7 +109,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
98
109
|
port: config.port
|
|
99
110
|
}, () => {
|
|
100
111
|
instance.server.off("error", onError);
|
|
101
|
-
instance.
|
|
112
|
+
instance.running = true;
|
|
102
113
|
const addr = instance.server.address();
|
|
103
114
|
if (!addr || typeof addr === "string") {
|
|
104
115
|
instance.cleanup();
|
|
@@ -114,9 +125,19 @@ var HealthzServer = class _HealthzServer {
|
|
|
114
125
|
});
|
|
115
126
|
});
|
|
116
127
|
}
|
|
117
|
-
/** Whether the
|
|
118
|
-
static
|
|
119
|
-
return _global[SINGLETON_KEY]?.
|
|
128
|
+
/** Whether the Healthz HTTP probe server is currently running and listening on its port. */
|
|
129
|
+
static isRunning() {
|
|
130
|
+
return _global[SINGLETON_KEY]?.running ?? false;
|
|
131
|
+
}
|
|
132
|
+
/** Mark application startup as completed (switches `/startupz` to 200). */
|
|
133
|
+
static markStartupComplete() {
|
|
134
|
+
const instance = _global[SINGLETON_KEY];
|
|
135
|
+
if (!instance) return;
|
|
136
|
+
instance.startupComplete = true;
|
|
137
|
+
}
|
|
138
|
+
/** Whether application startup has completed (for `/startupz`). */
|
|
139
|
+
static isStartupComplete() {
|
|
140
|
+
return _global[SINGLETON_KEY]?.startupComplete ?? false;
|
|
120
141
|
}
|
|
121
142
|
/** Mark the service as ready / not-ready for traffic. */
|
|
122
143
|
static setReady(ready) {
|
|
@@ -146,6 +167,34 @@ var HealthzServer = class _HealthzServer {
|
|
|
146
167
|
static getInstance() {
|
|
147
168
|
return _global[SINGLETON_KEY];
|
|
148
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Register a named check on this HealthzServer instance dynamically.
|
|
172
|
+
*
|
|
173
|
+
* @param check - The named check to register
|
|
174
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
175
|
+
* @returns This instance for chaining
|
|
176
|
+
*/
|
|
177
|
+
registerCheck(check, type = "readiness") {
|
|
178
|
+
if (type === "liveness" || type === "both") {
|
|
179
|
+
this.config.checks.push(check);
|
|
180
|
+
}
|
|
181
|
+
if (type === "readiness" || type === "both") {
|
|
182
|
+
this.config.readinessChecks ??= [
|
|
183
|
+
...this.config.checks
|
|
184
|
+
];
|
|
185
|
+
this.config.readinessChecks.push(check);
|
|
186
|
+
}
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Register a named check on the active singleton HealthzServer instance (if started).
|
|
191
|
+
*
|
|
192
|
+
* @param check - The named check to register
|
|
193
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
194
|
+
*/
|
|
195
|
+
static registerCheck(check, type = "readiness") {
|
|
196
|
+
_global[SINGLETON_KEY]?.registerCheck(check, type);
|
|
197
|
+
}
|
|
149
198
|
async handleRequest(req, res) {
|
|
150
199
|
const isHead = req.method === "HEAD";
|
|
151
200
|
if (req.method !== "GET" && !isHead) {
|
|
@@ -249,7 +298,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
249
298
|
}
|
|
250
299
|
/** `/startupz` — Startup probe */
|
|
251
300
|
handleStartup(res, isHead = false) {
|
|
252
|
-
if (this.
|
|
301
|
+
if (this.startupComplete) {
|
|
253
302
|
this.sendProbe(res, 200, "ok", [], isHead);
|
|
254
303
|
} else {
|
|
255
304
|
this.sendProbe(res, 503, "unhealthy", [
|
|
@@ -257,7 +306,7 @@ var HealthzServer = class _HealthzServer {
|
|
|
257
306
|
name: "startup",
|
|
258
307
|
ok: false,
|
|
259
308
|
durationMs: 0,
|
|
260
|
-
error: "
|
|
309
|
+
error: "Application startup not complete"
|
|
261
310
|
}
|
|
262
311
|
], isHead);
|
|
263
312
|
}
|
|
@@ -344,7 +393,8 @@ var HealthzServer = class _HealthzServer {
|
|
|
344
393
|
process.off("SIGTERM", this.onSigterm);
|
|
345
394
|
process.off("SIGINT", this.onSigint);
|
|
346
395
|
this.ready = false;
|
|
347
|
-
this.
|
|
396
|
+
this.startupComplete = false;
|
|
397
|
+
this.running = false;
|
|
348
398
|
this.shuttingDown = false;
|
|
349
399
|
}
|
|
350
400
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catbee/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A modular, production-grade utility toolkit for Node.js and TypeScript, designed for robust, scalable applications (including Express-based services). All utilities are tree-shakable and can be imported independently.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|