@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/server/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ var fs = require('@catbee/utils/fs');
|
|
|
39
39
|
var validation = require('@catbee/utils/validation');
|
|
40
40
|
var async = require('@catbee/utils/async');
|
|
41
41
|
var id = require('@catbee/utils/id');
|
|
42
|
+
var healthzServer = require('@catbee/utils/healthz-server');
|
|
42
43
|
|
|
43
44
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
44
45
|
|
|
@@ -307,36 +308,53 @@ var ServerConfigBuilder = class {
|
|
|
307
308
|
return this.setEnabled("requestLogging", false);
|
|
308
309
|
}
|
|
309
310
|
/**
|
|
310
|
-
* Configures server
|
|
311
|
+
* Configures the dedicated Healthz probe HTTP server for Kubernetes.
|
|
311
312
|
*
|
|
312
|
-
* @param opts -
|
|
313
|
+
* @param opts - Healthz server configuration options or boolean toggle
|
|
313
314
|
* @returns The builder instance for chaining
|
|
314
|
-
* @default - { path: '/healthz', detailed: true, withGlobalPrefix: false }
|
|
315
315
|
*
|
|
316
316
|
* @example
|
|
317
317
|
* ```typescript
|
|
318
|
-
* builder.
|
|
319
|
-
*
|
|
320
|
-
*
|
|
318
|
+
* builder.withHealthzServer({
|
|
319
|
+
* port: 8282,
|
|
320
|
+
* shutdownDelayMs: 5000,
|
|
321
|
+
* readinessChecks: [
|
|
322
|
+
* { name: 'db', check: () => checkDb() }
|
|
323
|
+
* ]
|
|
321
324
|
* })
|
|
322
325
|
* ```
|
|
323
326
|
*/
|
|
324
|
-
|
|
325
|
-
|
|
327
|
+
withHealthzServer(opts) {
|
|
328
|
+
if (typeof opts === "boolean") {
|
|
329
|
+
this.config.healthzServer = opts;
|
|
330
|
+
} else {
|
|
331
|
+
const current = object.isPlainObject(this.config.healthzServer) ? object.deepClone(this.config.healthzServer) : {};
|
|
332
|
+
this.config.healthzServer = object.deepObjMerge({}, current, {
|
|
333
|
+
enable: true,
|
|
334
|
+
...opts
|
|
335
|
+
});
|
|
336
|
+
}
|
|
326
337
|
return this;
|
|
327
338
|
}
|
|
328
339
|
/**
|
|
329
|
-
* Enables
|
|
330
|
-
*
|
|
340
|
+
* Enables the dedicated Healthz probe HTTP server.
|
|
341
|
+
*
|
|
342
|
+
* @param opts - Optional Healthz server configuration options
|
|
331
343
|
* @returns The builder instance for chaining
|
|
344
|
+
*/
|
|
345
|
+
enableHealthzServer(opts = {}) {
|
|
346
|
+
return this.withHealthzServer({
|
|
347
|
+
...opts,
|
|
348
|
+
enable: true
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Disables the dedicated Healthz probe HTTP server.
|
|
332
353
|
*
|
|
333
|
-
* @
|
|
334
|
-
* ```typescript
|
|
335
|
-
* builder.disableHealthCheck()
|
|
336
|
-
* ```
|
|
354
|
+
* @returns The builder instance for chaining
|
|
337
355
|
*/
|
|
338
|
-
|
|
339
|
-
return this.
|
|
356
|
+
disableHealthzServer() {
|
|
357
|
+
return this.withHealthzServer(false);
|
|
340
358
|
}
|
|
341
359
|
/**
|
|
342
360
|
* Configures OpenAPI/Swagger documentation for the API.
|
|
@@ -726,11 +744,12 @@ var ExpressServer = class {
|
|
|
726
744
|
gracefulShutdownRegistered = false;
|
|
727
745
|
/** Map of registered signal listeners for clean teardown */
|
|
728
746
|
signalListeners = /* @__PURE__ */ new Map();
|
|
729
|
-
/**
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
747
|
+
/** Running address info for the Healthz probe server */
|
|
748
|
+
healthzAddress;
|
|
749
|
+
/** Named checks queued for Healthz liveness probe */
|
|
750
|
+
healthzChecks = [];
|
|
751
|
+
/** Named checks queued for Healthz readiness probe */
|
|
752
|
+
healthzReadinessChecks = [];
|
|
734
753
|
/** Promise that resolves when initialization (middleware + routes) is complete */
|
|
735
754
|
initPromise;
|
|
736
755
|
/** In-flight start promise to protect against concurrent start() calls */
|
|
@@ -765,8 +784,19 @@ var ExpressServer = class {
|
|
|
765
784
|
logger.getLogger().error(msg);
|
|
766
785
|
throw new Error(msg);
|
|
767
786
|
}
|
|
768
|
-
if (config
|
|
769
|
-
this.
|
|
787
|
+
if (typeof this.config.healthzServer === "boolean") {
|
|
788
|
+
this.config.healthzServer = {
|
|
789
|
+
...healthzServer.HealthzServer.getDefaultConfig(),
|
|
790
|
+
enable: this.config.healthzServer
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (this.config.healthzServer && typeof this.config.healthzServer === "object") {
|
|
794
|
+
if (this.config.healthzServer.checks) {
|
|
795
|
+
this.healthzChecks.push(...this.config.healthzServer.checks);
|
|
796
|
+
}
|
|
797
|
+
if (this.config.healthzServer.readinessChecks) {
|
|
798
|
+
this.healthzReadinessChecks.push(...this.config.healthzServer.readinessChecks);
|
|
799
|
+
}
|
|
770
800
|
}
|
|
771
801
|
this.globalPrefix = this.normalizePath(this.config.globalPrefix ?? "", false);
|
|
772
802
|
this.hooks = hooks;
|
|
@@ -1114,10 +1144,6 @@ var ExpressServer = class {
|
|
|
1114
1144
|
* 4. Error handler
|
|
1115
1145
|
*/
|
|
1116
1146
|
async setupRoutes() {
|
|
1117
|
-
const healthCheckPath = this.normalizePath(this.config.healthCheck?.path || "/healthz", this.config.healthCheck?.withGlobalPrefix);
|
|
1118
|
-
this.app.get(healthCheckPath, async (_req, res) => {
|
|
1119
|
-
return this.handleHealthCheckRequest(res);
|
|
1120
|
-
});
|
|
1121
1147
|
this.app.use(this.globalPrefix, this.rootRouter);
|
|
1122
1148
|
await this.runHook("afterRoutes", this.app);
|
|
1123
1149
|
this.app.use((req, res) => {
|
|
@@ -1141,60 +1167,25 @@ var ExpressServer = class {
|
|
|
1141
1167
|
});
|
|
1142
1168
|
}
|
|
1143
1169
|
/**
|
|
1144
|
-
*
|
|
1170
|
+
* Whether the Healthz probe server is enabled.
|
|
1145
1171
|
*/
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
return res.status(httpStatusCodes.HttpStatusCodes.OK).json(new response.SuccessResponse("OK"));
|
|
1150
|
-
}
|
|
1151
|
-
const results = await this.executeHealthChecks();
|
|
1152
|
-
const allOk = results.every((r) => r.status);
|
|
1153
|
-
const status = allOk ? httpStatusCodes.HttpStatusCodes.OK : httpStatusCodes.HttpStatusCodes.SERVICE_UNAVAILABLE;
|
|
1154
|
-
const response$1 = new response.SuccessResponse(allOk ? "OK" : "Service unavailable");
|
|
1155
|
-
if (!allOk) response$1.error = true;
|
|
1156
|
-
if (this.config.healthCheck?.detailed) response$1.data = {
|
|
1157
|
-
checks: results
|
|
1158
|
-
};
|
|
1159
|
-
return res.status(status).json(response$1);
|
|
1160
|
-
} catch {
|
|
1161
|
-
return res.status(httpStatusCodes.HttpStatusCodes.INTERNAL_SERVER_ERROR).json(new exception.InternalServerErrorException("Health check failed"));
|
|
1172
|
+
isHealthzServerEnabled() {
|
|
1173
|
+
if (typeof this.config.healthzServer === "boolean") {
|
|
1174
|
+
return this.config.healthzServer;
|
|
1162
1175
|
}
|
|
1176
|
+
return this.config.healthzServer?.enable === true;
|
|
1163
1177
|
}
|
|
1164
1178
|
/**
|
|
1165
|
-
*
|
|
1179
|
+
* Get the graceful shutdown delay in milliseconds configured for HealthzServer.
|
|
1166
1180
|
*/
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
try {
|
|
1170
|
-
const status = await Promise.resolve(check());
|
|
1171
|
-
return {
|
|
1172
|
-
name,
|
|
1173
|
-
status,
|
|
1174
|
-
error: null
|
|
1175
|
-
};
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
return {
|
|
1178
|
-
name,
|
|
1179
|
-
status: false,
|
|
1180
|
-
error: error.message
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
}));
|
|
1184
|
-
return checkResults.map((result) => {
|
|
1185
|
-
if (result.status === "fulfilled") return result.value;
|
|
1186
|
-
return {
|
|
1187
|
-
name: "unknown",
|
|
1188
|
-
status: false,
|
|
1189
|
-
error: result.reason
|
|
1190
|
-
};
|
|
1191
|
-
});
|
|
1181
|
+
getHealthzShutdownDelay() {
|
|
1182
|
+
return typeof this.config.healthzServer === "object" ? this.config.healthzServer.shutdownDelayMs ?? 0 : 0;
|
|
1192
1183
|
}
|
|
1193
1184
|
/**
|
|
1194
|
-
* Register a new health check function for monitoring service dependencies.
|
|
1185
|
+
* Register a new health check function for monitoring service dependencies on the Healthz probe server.
|
|
1195
1186
|
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1187
|
+
* By default, external dependencies (DB, Redis, etc.) are registered as `readiness` checks.
|
|
1188
|
+
* Can also be registered as `liveness` or `both`.
|
|
1198
1189
|
*
|
|
1199
1190
|
* Examples:
|
|
1200
1191
|
* - Database connectivity
|
|
@@ -1203,33 +1194,76 @@ var ExpressServer = class {
|
|
|
1203
1194
|
* - Memory/CPU usage checks
|
|
1204
1195
|
*
|
|
1205
1196
|
* @param name Unique identifier for the check (used in detailed responses)
|
|
1206
|
-
* @param check Function returning boolean or Promise<boolean> indicating health
|
|
1197
|
+
* @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
|
|
1198
|
+
* @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
|
|
1207
1199
|
* @returns This instance for method chaining
|
|
1208
1200
|
*/
|
|
1209
|
-
registerHealthCheck(name, check) {
|
|
1210
|
-
|
|
1201
|
+
registerHealthCheck(name, check, options) {
|
|
1202
|
+
const probeType = typeof options === "string" ? options : options?.type ?? "readiness";
|
|
1203
|
+
const namedCheck = {
|
|
1211
1204
|
name,
|
|
1212
1205
|
check
|
|
1213
|
-
}
|
|
1206
|
+
};
|
|
1207
|
+
if (probeType === "liveness" || probeType === "both") {
|
|
1208
|
+
this.healthzChecks.push(namedCheck);
|
|
1209
|
+
}
|
|
1210
|
+
if (probeType === "readiness" || probeType === "both") {
|
|
1211
|
+
this.healthzReadinessChecks.push(namedCheck);
|
|
1212
|
+
}
|
|
1213
|
+
healthzServer.HealthzServer.registerCheck(namedCheck, probeType);
|
|
1214
1214
|
return this;
|
|
1215
1215
|
}
|
|
1216
1216
|
/**
|
|
1217
|
-
*
|
|
1218
|
-
* Useful for readiness probes in deployment tooling.
|
|
1217
|
+
* Mark the service as ready / not-ready for traffic on the Healthz probe server.
|
|
1219
1218
|
*
|
|
1220
|
-
* @
|
|
1219
|
+
* @param ready Whether the service is ready to receive traffic
|
|
1220
|
+
* @returns This instance for method chaining
|
|
1221
1221
|
*/
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1222
|
+
setReady(ready) {
|
|
1223
|
+
healthzServer.HealthzServer.setReady(ready);
|
|
1224
|
+
return this;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Whether the service is currently marked as ready for traffic on the Healthz probe server.
|
|
1228
|
+
*/
|
|
1229
|
+
isReady() {
|
|
1230
|
+
return healthzServer.HealthzServer.isReady();
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Mark application startup as completed on the Healthz probe server (switches `/startupz` to 200).
|
|
1234
|
+
*
|
|
1235
|
+
* @returns This instance for method chaining
|
|
1236
|
+
*/
|
|
1237
|
+
markStartupComplete() {
|
|
1238
|
+
healthzServer.HealthzServer.markStartupComplete();
|
|
1239
|
+
return this;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Whether application startup has completed on the Healthz probe server.
|
|
1243
|
+
*/
|
|
1244
|
+
isStartupComplete() {
|
|
1245
|
+
return healthzServer.HealthzServer.isStartupComplete();
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Get the running HealthzServer instance (if started).
|
|
1249
|
+
*/
|
|
1250
|
+
getHealthzServer() {
|
|
1251
|
+
return healthzServer.HealthzServer.getInstance();
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Get the address info of the running HealthzServer (if started).
|
|
1255
|
+
*/
|
|
1256
|
+
getHealthzAddress() {
|
|
1257
|
+
return this.healthzAddress;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Returns whether the service is currently marked as ready for traffic on the Healthz probe server.
|
|
1261
|
+
* Useful for readiness checks in deployment tooling.
|
|
1262
|
+
*
|
|
1263
|
+
* @returns `true` when ready, otherwise `false`.
|
|
1264
|
+
*/
|
|
1265
|
+
ready() {
|
|
1266
|
+
return healthzServer.HealthzServer.isReady();
|
|
1233
1267
|
}
|
|
1234
1268
|
/**
|
|
1235
1269
|
* Get the underlying Express application instance.
|
|
@@ -1286,48 +1320,105 @@ var ExpressServer = class {
|
|
|
1286
1320
|
*/
|
|
1287
1321
|
async doStart() {
|
|
1288
1322
|
await this.initPromise;
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1323
|
+
if (this.isHealthzServerEnabled()) {
|
|
1324
|
+
const healthzConfig = {
|
|
1325
|
+
...typeof this.config.healthzServer === "object" ? this.config.healthzServer : {},
|
|
1326
|
+
handleSignals: false,
|
|
1327
|
+
checks: [
|
|
1328
|
+
...this.healthzChecks
|
|
1329
|
+
],
|
|
1330
|
+
readinessChecks: [
|
|
1331
|
+
...this.healthzReadinessChecks
|
|
1332
|
+
]
|
|
1333
|
+
};
|
|
1334
|
+
const addr = await healthzServer.HealthzServer.start(healthzConfig);
|
|
1335
|
+
if (!addr) {
|
|
1336
|
+
throw new Error("Healthz probe server failed to start (already running in this process)");
|
|
1337
|
+
}
|
|
1338
|
+
this.healthzAddress = addr;
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
await this.runHook("beforeStart", this.app);
|
|
1342
|
+
const server = this.createServerInstance();
|
|
1343
|
+
this.server = server;
|
|
1344
|
+
return await new Promise((resolve, reject) => {
|
|
1345
|
+
let isListening = false;
|
|
1346
|
+
server.on("error", async (err) => {
|
|
1347
|
+
if (!isListening) {
|
|
1348
|
+
logger.getLogger().error({
|
|
1349
|
+
err
|
|
1350
|
+
}, "Server failed to start");
|
|
1351
|
+
try {
|
|
1352
|
+
server.removeAllListeners();
|
|
1353
|
+
server.close();
|
|
1354
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1355
|
+
await healthzServer.HealthzServer.stop().catch(() => {
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
this.server = null;
|
|
1361
|
+
this.healthzAddress = null;
|
|
1362
|
+
this.connections.clear();
|
|
1363
|
+
reject(err);
|
|
1364
|
+
} else {
|
|
1365
|
+
logger.getLogger().error({
|
|
1366
|
+
err
|
|
1367
|
+
}, "Server runtime error");
|
|
1303
1368
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1369
|
+
});
|
|
1370
|
+
this.setupConnectionTracking();
|
|
1371
|
+
Promise.resolve(this.runHook("onServerCreated", server)).then(() => {
|
|
1372
|
+
const onListening = /* @__PURE__ */ __name(async () => {
|
|
1373
|
+
try {
|
|
1374
|
+
this.logServerStartInfo();
|
|
1375
|
+
await this.runHook("afterStart", server);
|
|
1376
|
+
if (this.isHealthzServerEnabled()) {
|
|
1377
|
+
healthzServer.HealthzServer.markStartupComplete();
|
|
1378
|
+
healthzServer.HealthzServer.setReady(true);
|
|
1379
|
+
}
|
|
1380
|
+
isListening = true;
|
|
1381
|
+
resolve(server);
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1384
|
+
logger.getLogger().error({
|
|
1385
|
+
err: error
|
|
1386
|
+
}, "Server startup failed");
|
|
1387
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1388
|
+
await healthzServer.HealthzServer.stop().catch(() => {
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
server.removeAllListeners();
|
|
1393
|
+
server.close();
|
|
1394
|
+
} catch {
|
|
1395
|
+
}
|
|
1396
|
+
this.server = null;
|
|
1397
|
+
this.healthzAddress = null;
|
|
1398
|
+
this.connections.clear();
|
|
1399
|
+
reject(error);
|
|
1400
|
+
}
|
|
1401
|
+
}, "onListening");
|
|
1402
|
+
const listenArgs = [
|
|
1403
|
+
this.config.port,
|
|
1404
|
+
this.config.host,
|
|
1405
|
+
onListening
|
|
1406
|
+
];
|
|
1407
|
+
server.listen(...listenArgs);
|
|
1408
|
+
}).catch((err) => {
|
|
1409
|
+
server.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
1410
|
+
});
|
|
1329
1411
|
});
|
|
1330
|
-
})
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1414
|
+
await healthzServer.HealthzServer.stop().catch(() => {
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
this.healthzAddress = null;
|
|
1418
|
+
this.server = null;
|
|
1419
|
+
this.connections.clear();
|
|
1420
|
+
throw err;
|
|
1421
|
+
}
|
|
1331
1422
|
}
|
|
1332
1423
|
/**
|
|
1333
1424
|
* Create HTTP or HTTPS server instance (without listening).
|
|
@@ -1367,8 +1458,8 @@ var ExpressServer = class {
|
|
|
1367
1458
|
const host = this.formatHostForUrl(this.config.host || "localhost");
|
|
1368
1459
|
const url = `${protocol}://${host}:${port}`;
|
|
1369
1460
|
logger.getLogger().info(`Server running on ${url}`);
|
|
1370
|
-
if (this.
|
|
1371
|
-
logger.getLogger().info(`
|
|
1461
|
+
if (this.healthzAddress) {
|
|
1462
|
+
logger.getLogger().info(`Healthz probe server running on http://${this.healthzAddress.address}:${this.healthzAddress.port}`);
|
|
1372
1463
|
}
|
|
1373
1464
|
if (this.config.openApi?.enable) {
|
|
1374
1465
|
logger.getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
|
|
@@ -1391,7 +1482,7 @@ var ExpressServer = class {
|
|
|
1391
1482
|
* - Monitoring systems are notified
|
|
1392
1483
|
*/
|
|
1393
1484
|
async stop(force = false) {
|
|
1394
|
-
if (!this.server) {
|
|
1485
|
+
if (!this.server && !healthzServer.HealthzServer.isRunning()) {
|
|
1395
1486
|
logger.getLogger().warn("Stop called but server is not running");
|
|
1396
1487
|
return;
|
|
1397
1488
|
}
|
|
@@ -1400,10 +1491,26 @@ var ExpressServer = class {
|
|
|
1400
1491
|
return;
|
|
1401
1492
|
}
|
|
1402
1493
|
this.isShuttingDown = true;
|
|
1403
|
-
|
|
1494
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1495
|
+
healthzServer.HealthzServer.setReady(false);
|
|
1496
|
+
}
|
|
1497
|
+
if (this.server) {
|
|
1498
|
+
await this.runHook("beforeStop", this.server);
|
|
1499
|
+
}
|
|
1404
1500
|
try {
|
|
1405
|
-
|
|
1501
|
+
const shutdownDelay = this.getHealthzShutdownDelay();
|
|
1502
|
+
if (shutdownDelay > 0 && !force && healthzServer.HealthzServer.isRunning()) {
|
|
1503
|
+
logger.getLogger().info(`Waiting ${shutdownDelay}ms for load balancer to drain traffic...`);
|
|
1504
|
+
await new Promise((resolve) => setTimeout(resolve, shutdownDelay));
|
|
1505
|
+
}
|
|
1506
|
+
if (this.server) {
|
|
1507
|
+
await this.gracefulShutdown(force);
|
|
1508
|
+
}
|
|
1406
1509
|
} finally {
|
|
1510
|
+
if (healthzServer.HealthzServer.isRunning()) {
|
|
1511
|
+
await healthzServer.HealthzServer.stop();
|
|
1512
|
+
this.healthzAddress = null;
|
|
1513
|
+
}
|
|
1407
1514
|
this.isShuttingDown = false;
|
|
1408
1515
|
}
|
|
1409
1516
|
}
|
|
@@ -1492,7 +1599,7 @@ var ExpressServer = class {
|
|
|
1492
1599
|
logger.getLogger().info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1493
1600
|
try {
|
|
1494
1601
|
this.disableGracefulShutdown();
|
|
1495
|
-
await this.stop(
|
|
1602
|
+
await this.stop(false);
|
|
1496
1603
|
process.exit(0);
|
|
1497
1604
|
} catch (err) {
|
|
1498
1605
|
logger.getLogger().fatal({
|
package/server/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ import express, { Express, Router } from 'express';
|
|
|
26
26
|
import http from 'node:http';
|
|
27
27
|
import https from 'node:https';
|
|
28
28
|
import { CatbeeServerConfig, CatbeeServerHooks } from '@catbee/utils/types';
|
|
29
|
+
import { HealthzServer, HealthzAddressInfo, CatbeeHealthzServerConfig } from '@catbee/utils/healthz-server';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Map of critical dependencies to their error messages.
|
|
@@ -76,11 +77,12 @@ declare class ExpressServer {
|
|
|
76
77
|
private gracefulShutdownRegistered;
|
|
77
78
|
/** Map of registered signal listeners for clean teardown */
|
|
78
79
|
private readonly signalListeners;
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
/** Running address info for the Healthz probe server */
|
|
81
|
+
private healthzAddress?;
|
|
82
|
+
/** Named checks queued for Healthz liveness probe */
|
|
83
|
+
private readonly healthzChecks;
|
|
84
|
+
/** Named checks queued for Healthz readiness probe */
|
|
85
|
+
private readonly healthzReadinessChecks;
|
|
84
86
|
/** Promise that resolves when initialization (middleware + routes) is complete */
|
|
85
87
|
private readonly initPromise;
|
|
86
88
|
/** In-flight start promise to protect against concurrent start() calls */
|
|
@@ -197,18 +199,18 @@ declare class ExpressServer {
|
|
|
197
199
|
*/
|
|
198
200
|
protected setupRoutes(): Promise<void>;
|
|
199
201
|
/**
|
|
200
|
-
*
|
|
202
|
+
* Whether the Healthz probe server is enabled.
|
|
201
203
|
*/
|
|
202
|
-
|
|
204
|
+
isHealthzServerEnabled(): boolean;
|
|
203
205
|
/**
|
|
204
|
-
*
|
|
206
|
+
* Get the graceful shutdown delay in milliseconds configured for HealthzServer.
|
|
205
207
|
*/
|
|
206
|
-
private
|
|
208
|
+
private getHealthzShutdownDelay;
|
|
207
209
|
/**
|
|
208
|
-
* Register a new health check function for monitoring service dependencies.
|
|
210
|
+
* Register a new health check function for monitoring service dependencies on the Healthz probe server.
|
|
209
211
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
+
* By default, external dependencies (DB, Redis, etc.) are registered as `readiness` checks.
|
|
213
|
+
* Can also be registered as `liveness` or `both`.
|
|
212
214
|
*
|
|
213
215
|
* Examples:
|
|
214
216
|
* - Database connectivity
|
|
@@ -217,17 +219,49 @@ declare class ExpressServer {
|
|
|
217
219
|
* - Memory/CPU usage checks
|
|
218
220
|
*
|
|
219
221
|
* @param name Unique identifier for the check (used in detailed responses)
|
|
220
|
-
* @param check Function returning boolean or Promise<boolean> indicating health
|
|
222
|
+
* @param check Function returning boolean or Promise<boolean> indicating health (supports optional AbortSignal)
|
|
223
|
+
* @param options Target probe type ('readiness' | 'liveness' | 'both') or options object
|
|
224
|
+
* @returns This instance for method chaining
|
|
225
|
+
*/
|
|
226
|
+
registerHealthCheck(name: string, check: (signal?: AbortSignal) => Promise<boolean> | boolean, options?: 'readiness' | 'liveness' | 'both' | {
|
|
227
|
+
type?: 'readiness' | 'liveness' | 'both';
|
|
228
|
+
}): this;
|
|
229
|
+
/**
|
|
230
|
+
* Mark the service as ready / not-ready for traffic on the Healthz probe server.
|
|
231
|
+
*
|
|
232
|
+
* @param ready Whether the service is ready to receive traffic
|
|
233
|
+
* @returns This instance for method chaining
|
|
234
|
+
*/
|
|
235
|
+
setReady(ready: boolean): this;
|
|
236
|
+
/**
|
|
237
|
+
* Whether the service is currently marked as ready for traffic on the Healthz probe server.
|
|
238
|
+
*/
|
|
239
|
+
isReady(): boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Mark application startup as completed on the Healthz probe server (switches `/startupz` to 200).
|
|
242
|
+
*
|
|
221
243
|
* @returns This instance for method chaining
|
|
222
244
|
*/
|
|
223
|
-
|
|
245
|
+
markStartupComplete(): this;
|
|
224
246
|
/**
|
|
225
|
-
*
|
|
226
|
-
|
|
247
|
+
* Whether application startup has completed on the Healthz probe server.
|
|
248
|
+
*/
|
|
249
|
+
isStartupComplete(): boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Get the running HealthzServer instance (if started).
|
|
252
|
+
*/
|
|
253
|
+
getHealthzServer(): HealthzServer | undefined;
|
|
254
|
+
/**
|
|
255
|
+
* Get the address info of the running HealthzServer (if started).
|
|
256
|
+
*/
|
|
257
|
+
getHealthzAddress(): HealthzAddressInfo | null | undefined;
|
|
258
|
+
/**
|
|
259
|
+
* Returns whether the service is currently marked as ready for traffic on the Healthz probe server.
|
|
260
|
+
* Useful for readiness checks in deployment tooling.
|
|
227
261
|
*
|
|
228
|
-
* @returns
|
|
262
|
+
* @returns `true` when ready, otherwise `false`.
|
|
229
263
|
*/
|
|
230
|
-
ready():
|
|
264
|
+
ready(): boolean;
|
|
231
265
|
/**
|
|
232
266
|
* Get the underlying Express application instance.
|
|
233
267
|
* Use this for advanced Express features not exposed by this wrapper.
|
|
@@ -715,32 +749,36 @@ declare class ServerConfigBuilder {
|
|
|
715
749
|
*/
|
|
716
750
|
disableRequestLogging(): this;
|
|
717
751
|
/**
|
|
718
|
-
* Configures server
|
|
752
|
+
* Configures the dedicated Healthz probe HTTP server for Kubernetes.
|
|
719
753
|
*
|
|
720
|
-
* @param opts -
|
|
754
|
+
* @param opts - Healthz server configuration options or boolean toggle
|
|
721
755
|
* @returns The builder instance for chaining
|
|
722
|
-
* @default - { path: '/healthz', detailed: true, withGlobalPrefix: false }
|
|
723
756
|
*
|
|
724
757
|
* @example
|
|
725
758
|
* ```typescript
|
|
726
|
-
* builder.
|
|
727
|
-
*
|
|
728
|
-
*
|
|
759
|
+
* builder.withHealthzServer({
|
|
760
|
+
* port: 8282,
|
|
761
|
+
* shutdownDelayMs: 5000,
|
|
762
|
+
* readinessChecks: [
|
|
763
|
+
* { name: 'db', check: () => checkDb() }
|
|
764
|
+
* ]
|
|
729
765
|
* })
|
|
730
766
|
* ```
|
|
731
767
|
*/
|
|
732
|
-
|
|
768
|
+
withHealthzServer(opts: NonNullable<CatbeeServerConfig['healthzServer']>): this;
|
|
733
769
|
/**
|
|
734
|
-
* Enables
|
|
735
|
-
*
|
|
770
|
+
* Enables the dedicated Healthz probe HTTP server.
|
|
771
|
+
*
|
|
772
|
+
* @param opts - Optional Healthz server configuration options
|
|
736
773
|
* @returns The builder instance for chaining
|
|
774
|
+
*/
|
|
775
|
+
enableHealthzServer(opts?: Partial<CatbeeHealthzServerConfig>): this;
|
|
776
|
+
/**
|
|
777
|
+
* Disables the dedicated Healthz probe HTTP server.
|
|
737
778
|
*
|
|
738
|
-
* @
|
|
739
|
-
* ```typescript
|
|
740
|
-
* builder.disableHealthCheck()
|
|
741
|
-
* ```
|
|
779
|
+
* @returns The builder instance for chaining
|
|
742
780
|
*/
|
|
743
|
-
|
|
781
|
+
disableHealthzServer(): this;
|
|
744
782
|
/**
|
|
745
783
|
* Configures OpenAPI/Swagger documentation for the API.
|
|
746
784
|
*
|