@catbee/utils 2.0.5 → 2.2.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/README.md +2 -1
- package/config/index.cjs +5 -6
- package/config/index.mjs +5 -6
- package/context-store/index.cjs +97 -76
- package/context-store/index.d.ts +74 -57
- package/context-store/index.mjs +97 -77
- package/healthz-server/index.cjs +393 -0
- package/healthz-server/index.d.ts +317 -0
- package/healthz-server/index.mjs +389 -0
- package/index.cjs +7 -0
- package/index.d.ts +1 -0
- package/index.mjs +1 -0
- package/logger/index.cjs +174 -76
- package/logger/index.d.ts +29 -18
- package/logger/index.mjs +174 -77
- package/package.json +12 -7
- package/server/index.cjs +393 -172
- package/server/index.d.ts +132 -46
- package/server/index.mjs +395 -175
- package/types/index.d.ts +142 -47
package/context-store/index.mjs
CHANGED
|
@@ -26,23 +26,94 @@ import { AsyncLocalStorage } from 'async_hooks';
|
|
|
26
26
|
|
|
27
27
|
var __defProp = Object.defineProperty;
|
|
28
28
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
29
|
+
var TypedContextKey = class {
|
|
30
|
+
static {
|
|
31
|
+
__name(this, "TypedContextKey");
|
|
32
|
+
}
|
|
33
|
+
symbol;
|
|
34
|
+
defaultValue;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new typed context key.
|
|
37
|
+
* @param symbol - The unique symbol for this key
|
|
38
|
+
* @param defaultValue - Optional default value if key is not found
|
|
39
|
+
*/
|
|
40
|
+
constructor(symbol, defaultValue) {
|
|
41
|
+
this.symbol = symbol;
|
|
42
|
+
this.defaultValue = defaultValue;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Gets the current value for this key.
|
|
46
|
+
* @returns The value or defaultValue if not found
|
|
47
|
+
*/
|
|
48
|
+
get() {
|
|
49
|
+
return ContextStore.get(this.symbol) ?? this.defaultValue;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sets the value for this key.
|
|
53
|
+
* @param value The value to set
|
|
54
|
+
*/
|
|
55
|
+
set(value) {
|
|
56
|
+
ContextStore.set(this.symbol, value);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Checks if this key exists in the context.
|
|
60
|
+
* @returns True if the key exists
|
|
61
|
+
*/
|
|
62
|
+
exists() {
|
|
63
|
+
return ContextStore.has(this.symbol);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Deletes this key from the context.
|
|
67
|
+
* @returns True if the key was deleted
|
|
68
|
+
*/
|
|
69
|
+
delete() {
|
|
70
|
+
return ContextStore.delete(this.symbol);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Gets the symbol for this key.
|
|
74
|
+
* @returns Symbol for this key
|
|
75
|
+
*/
|
|
76
|
+
getSymbol() {
|
|
77
|
+
return this.symbol;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
29
80
|
var StoreKeys = {
|
|
30
81
|
LOGGER: Symbol("LOGGER"),
|
|
31
82
|
REQUEST_ID: Symbol("REQUEST_ID"),
|
|
32
|
-
|
|
33
|
-
SESSION: Symbol("SESSION"),
|
|
34
|
-
TRANSACTION_ID: Symbol("TRANSACTION_ID"),
|
|
83
|
+
CORRELATION_ID: Symbol("CORRELATION_ID"),
|
|
35
84
|
USER_ID: Symbol("USER_ID"),
|
|
85
|
+
TRANSACTION_ID: Symbol("TRANSACTION_ID"),
|
|
36
86
|
TENANT_ID: Symbol("TENANT_ID"),
|
|
37
87
|
TRACE_ID: Symbol("TRACE_ID"),
|
|
38
|
-
|
|
88
|
+
SPAN_ID: Symbol("SPAN_ID"),
|
|
89
|
+
MESSAGE_ID: Symbol("MESSAGE_ID"),
|
|
90
|
+
MESSAGE_TYPE: Symbol("MESSAGE_TYPE"),
|
|
91
|
+
QUEUE_NAME: Symbol("QUEUE_NAME")
|
|
92
|
+
};
|
|
93
|
+
var TypedStoreKeys = {
|
|
94
|
+
LOGGER: new TypedContextKey(StoreKeys.LOGGER),
|
|
95
|
+
REQUEST_ID: new TypedContextKey(StoreKeys.REQUEST_ID),
|
|
96
|
+
CORRELATION_ID: new TypedContextKey(StoreKeys.CORRELATION_ID),
|
|
97
|
+
USER_ID: new TypedContextKey(StoreKeys.USER_ID),
|
|
98
|
+
TRANSACTION_ID: new TypedContextKey(StoreKeys.TRANSACTION_ID),
|
|
99
|
+
TENANT_ID: new TypedContextKey(StoreKeys.TENANT_ID),
|
|
100
|
+
TRACE_ID: new TypedContextKey(StoreKeys.TRACE_ID),
|
|
101
|
+
SPAN_ID: new TypedContextKey(StoreKeys.SPAN_ID),
|
|
102
|
+
MESSAGE_ID: new TypedContextKey(StoreKeys.MESSAGE_ID),
|
|
103
|
+
MESSAGE_TYPE: new TypedContextKey(StoreKeys.MESSAGE_TYPE),
|
|
104
|
+
QUEUE_NAME: new TypedContextKey(StoreKeys.QUEUE_NAME)
|
|
39
105
|
};
|
|
40
106
|
function getRequestId() {
|
|
41
|
-
return
|
|
107
|
+
return TypedStoreKeys.REQUEST_ID.get();
|
|
42
108
|
}
|
|
43
109
|
__name(getRequestId, "getRequestId");
|
|
110
|
+
function resolveKey(key) {
|
|
111
|
+
return key instanceof TypedContextKey ? key.getSymbol() : key;
|
|
112
|
+
}
|
|
113
|
+
__name(resolveKey, "resolveKey");
|
|
44
114
|
function getFromContext(key) {
|
|
45
|
-
|
|
115
|
+
const symbol = resolveKey(key);
|
|
116
|
+
return ContextStore.get(symbol);
|
|
46
117
|
}
|
|
47
118
|
__name(getFromContext, "getFromContext");
|
|
48
119
|
var ContextStore = class _ContextStore {
|
|
@@ -70,8 +141,9 @@ var ContextStore = class _ContextStore {
|
|
|
70
141
|
* @returns {T | undefined} The value found (typed) or undefined if not present.
|
|
71
142
|
*/
|
|
72
143
|
static get(key) {
|
|
144
|
+
const symbol = resolveKey(key);
|
|
73
145
|
const store = this.storage.getStore();
|
|
74
|
-
return store?.[
|
|
146
|
+
return store?.[symbol];
|
|
75
147
|
}
|
|
76
148
|
/**
|
|
77
149
|
* Sets a value in the current context store by symbol key.
|
|
@@ -82,11 +154,12 @@ var ContextStore = class _ContextStore {
|
|
|
82
154
|
* @throws {Error} If called outside an active context (not within a .run call or in the wrong async boundaries).
|
|
83
155
|
*/
|
|
84
156
|
static set(key, value) {
|
|
157
|
+
const symbol = resolveKey(key);
|
|
85
158
|
const store = this.storage.getStore();
|
|
86
159
|
if (!store) {
|
|
87
|
-
throw new Error(`Failed to set ${String(
|
|
160
|
+
throw new Error(`Failed to set ${String(symbol)}: AsyncLocalStorage store is not initialized.`);
|
|
88
161
|
}
|
|
89
|
-
store[
|
|
162
|
+
store[symbol] = value;
|
|
90
163
|
}
|
|
91
164
|
/**
|
|
92
165
|
* Retrieves the entire context store object for the current async context.
|
|
@@ -115,8 +188,9 @@ var ContextStore = class _ContextStore {
|
|
|
115
188
|
* @returns {boolean} True if the key exists, false otherwise.
|
|
116
189
|
*/
|
|
117
190
|
static has(key) {
|
|
191
|
+
const symbol = resolveKey(key);
|
|
118
192
|
const store = this.storage.getStore();
|
|
119
|
-
return store !== void 0 &&
|
|
193
|
+
return store !== void 0 && symbol in store;
|
|
120
194
|
}
|
|
121
195
|
/**
|
|
122
196
|
* Removes a value from the current context store by symbol key.
|
|
@@ -126,11 +200,12 @@ var ContextStore = class _ContextStore {
|
|
|
126
200
|
* @throws {Error} If called outside an active context.
|
|
127
201
|
*/
|
|
128
202
|
static delete(key) {
|
|
203
|
+
const symbol = resolveKey(key);
|
|
129
204
|
const store = this.storage.getStore();
|
|
130
205
|
if (!store) {
|
|
131
|
-
throw new Error(`Failed to delete ${String(
|
|
206
|
+
throw new Error(`Failed to delete ${String(symbol)}: AsyncLocalStorage store is not initialized.`);
|
|
132
207
|
}
|
|
133
|
-
return delete store[
|
|
208
|
+
return delete store[symbol];
|
|
134
209
|
}
|
|
135
210
|
/**
|
|
136
211
|
* Updates multiple values in the current context store at once.
|
|
@@ -159,22 +234,16 @@ var ContextStore = class _ContextStore {
|
|
|
159
234
|
* @throws {Error} If called outside an active context.
|
|
160
235
|
*/
|
|
161
236
|
static withValue(key, value, callback) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const hasOriginal = key in store;
|
|
167
|
-
const originalValue = store[key];
|
|
168
|
-
store[key] = value;
|
|
169
|
-
try {
|
|
170
|
-
return callback();
|
|
171
|
-
} finally {
|
|
172
|
-
if (hasOriginal) {
|
|
173
|
-
store[key] = originalValue;
|
|
174
|
-
} else {
|
|
175
|
-
delete store[key];
|
|
176
|
-
}
|
|
237
|
+
const symbol = resolveKey(key);
|
|
238
|
+
const currentStore = this.storage.getStore();
|
|
239
|
+
if (!currentStore) {
|
|
240
|
+
throw new Error("Failed to set temporary value: AsyncLocalStorage store is not initialized.");
|
|
177
241
|
}
|
|
242
|
+
const newStore = {
|
|
243
|
+
...currentStore,
|
|
244
|
+
[symbol]: value
|
|
245
|
+
};
|
|
246
|
+
return this.storage.run(newStore, callback);
|
|
178
247
|
}
|
|
179
248
|
/**
|
|
180
249
|
* Creates a new context that inherits values from the current context.
|
|
@@ -207,54 +276,5 @@ var ContextStore = class _ContextStore {
|
|
|
207
276
|
};
|
|
208
277
|
}
|
|
209
278
|
};
|
|
210
|
-
var TypedContextKey = class {
|
|
211
|
-
static {
|
|
212
|
-
__name(this, "TypedContextKey");
|
|
213
|
-
}
|
|
214
|
-
symbol;
|
|
215
|
-
defaultValue;
|
|
216
|
-
/**
|
|
217
|
-
* Creates a new typed context key.
|
|
218
|
-
*
|
|
219
|
-
* @param symbol - The unique symbol for this key
|
|
220
|
-
* @param defaultValue - Optional default value if key is not found
|
|
221
|
-
*/
|
|
222
|
-
constructor(symbol, defaultValue) {
|
|
223
|
-
this.symbol = symbol;
|
|
224
|
-
this.defaultValue = defaultValue;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Gets the current value for this key.
|
|
228
|
-
*
|
|
229
|
-
* @returns The value or defaultValue if not found
|
|
230
|
-
*/
|
|
231
|
-
get() {
|
|
232
|
-
return ContextStore.get(this.symbol) ?? this.defaultValue;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Sets the value for this key.
|
|
236
|
-
*
|
|
237
|
-
* @param value The value to set
|
|
238
|
-
*/
|
|
239
|
-
set(value) {
|
|
240
|
-
ContextStore.set(this.symbol, value);
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Checks if this key exists in the context.
|
|
244
|
-
*
|
|
245
|
-
* @returns True if the key exists
|
|
246
|
-
*/
|
|
247
|
-
exists() {
|
|
248
|
-
return ContextStore.has(this.symbol);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Deletes this key from the context.
|
|
252
|
-
*
|
|
253
|
-
* @returns True if the key was deleted
|
|
254
|
-
*/
|
|
255
|
-
delete() {
|
|
256
|
-
return ContextStore.delete(this.symbol);
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
279
|
|
|
260
|
-
export { ContextStore, StoreKeys, TypedContextKey, getFromContext, getRequestId };
|
|
280
|
+
export { ContextStore, StoreKeys, TypedContextKey, TypedStoreKeys, getFromContext, getRequestId };
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The MIT License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
var http = require('http');
|
|
28
|
+
var env = require('@catbee/utils/env');
|
|
29
|
+
|
|
30
|
+
var __defProp = Object.defineProperty;
|
|
31
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
32
|
+
function getDefaultHealthzConfig() {
|
|
33
|
+
return {
|
|
34
|
+
host: env.Env.get("HEALTHZ_HOST", "") || env.Env.get("SERVER_HEALTHZ_HOST", "") || env.Env.get("SERVER_HOST", "") || env.Env.get("HOST", "0.0.0.0"),
|
|
35
|
+
port: env.Env.getPort("HEALTHZ_PORT", env.Env.getPort("SERVER_HEALTHZ_PORT", 8282)),
|
|
36
|
+
healthzPath: env.Env.get("HEALTHZ_PATH", "") || env.Env.get("SERVER_HEALTHZ_PATH", "") || env.Env.get("SERVER_HEALTH_CHECK_PATH", "/healthz"),
|
|
37
|
+
readyzPath: env.Env.get("HEALTHZ_READYZ_PATH", "") || env.Env.get("SERVER_READYZ_PATH", "/readyz"),
|
|
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_HEALTHZ_DETAILED", env.Env.getBoolean("SERVER_HEALTH_CHECK_DETAILED_OUTPUT", true))),
|
|
40
|
+
checks: [],
|
|
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
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
__name(getDefaultHealthzConfig, "getDefaultHealthzConfig");
|
|
47
|
+
function resolveConfig(userConfig) {
|
|
48
|
+
const defaults = getDefaultHealthzConfig();
|
|
49
|
+
if (!userConfig) return {
|
|
50
|
+
...defaults
|
|
51
|
+
};
|
|
52
|
+
const cleaned = Object.fromEntries(Object.entries(userConfig).filter(([, v]) => v !== void 0));
|
|
53
|
+
return {
|
|
54
|
+
...defaults,
|
|
55
|
+
...cleaned
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
__name(resolveConfig, "resolveConfig");
|
|
59
|
+
|
|
60
|
+
// src/healthz-server/healthz-server.ts
|
|
61
|
+
var SINGLETON_KEY = Symbol.for("CatbeeHealthzServer");
|
|
62
|
+
var _global = globalThis;
|
|
63
|
+
var HealthzServer = class _HealthzServer {
|
|
64
|
+
static {
|
|
65
|
+
__name(this, "HealthzServer");
|
|
66
|
+
}
|
|
67
|
+
server;
|
|
68
|
+
config;
|
|
69
|
+
startedAt = Date.now();
|
|
70
|
+
/** Whether the health server has successfully started listening (for `/startupz`) */
|
|
71
|
+
started = false;
|
|
72
|
+
/** Whether the service is ready to receive traffic (for `/readyz`) */
|
|
73
|
+
ready = false;
|
|
74
|
+
shuttingDown = false;
|
|
75
|
+
onSigterm = /* @__PURE__ */ __name(() => this.initiateShutdown(), "onSigterm");
|
|
76
|
+
onSigint = /* @__PURE__ */ __name(() => this.initiateShutdown(), "onSigint");
|
|
77
|
+
constructor(config) {
|
|
78
|
+
this.config = config;
|
|
79
|
+
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
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();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Boot the health-check server.
|
|
93
|
+
* Returns `null` if a server is already running in this process.
|
|
94
|
+
*/
|
|
95
|
+
static async start(opts) {
|
|
96
|
+
if (_global[SINGLETON_KEY]) return null;
|
|
97
|
+
const config = resolveConfig(opts);
|
|
98
|
+
const instance = new _HealthzServer(config);
|
|
99
|
+
_global[SINGLETON_KEY] = instance;
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const onError = /* @__PURE__ */ __name((err) => {
|
|
102
|
+
instance.cleanup();
|
|
103
|
+
delete _global[SINGLETON_KEY];
|
|
104
|
+
reject(err);
|
|
105
|
+
}, "onError");
|
|
106
|
+
instance.server.once("error", onError);
|
|
107
|
+
instance.server.listen({
|
|
108
|
+
host: config.host,
|
|
109
|
+
port: config.port
|
|
110
|
+
}, () => {
|
|
111
|
+
instance.server.off("error", onError);
|
|
112
|
+
instance.started = true;
|
|
113
|
+
const addr = instance.server.address();
|
|
114
|
+
if (!addr || typeof addr === "string") {
|
|
115
|
+
instance.cleanup();
|
|
116
|
+
delete _global[SINGLETON_KEY];
|
|
117
|
+
reject(new Error("Failed to resolve health server address"));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
resolve({
|
|
121
|
+
address: addr.address,
|
|
122
|
+
family: addr.family,
|
|
123
|
+
port: addr.port
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Whether the health-check server is currently running and started. */
|
|
129
|
+
static isStarted() {
|
|
130
|
+
return _global[SINGLETON_KEY]?.started ?? false;
|
|
131
|
+
}
|
|
132
|
+
/** Mark the service as ready / not-ready for traffic. */
|
|
133
|
+
static setReady(ready) {
|
|
134
|
+
const instance = _global[SINGLETON_KEY];
|
|
135
|
+
if (!instance) return;
|
|
136
|
+
instance.ready = ready;
|
|
137
|
+
}
|
|
138
|
+
/** Whether the service is currently marked as ready. */
|
|
139
|
+
static isReady() {
|
|
140
|
+
return _global[SINGLETON_KEY]?.ready ?? false;
|
|
141
|
+
}
|
|
142
|
+
/** Gracefully stop the health-check server. */
|
|
143
|
+
static async stop() {
|
|
144
|
+
const instance = _global[SINGLETON_KEY];
|
|
145
|
+
if (!instance) return;
|
|
146
|
+
instance.cleanup();
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
if (typeof instance.server.closeIdleConnections === "function") {
|
|
149
|
+
instance.server.closeIdleConnections();
|
|
150
|
+
}
|
|
151
|
+
instance.server.close(() => {
|
|
152
|
+
delete _global[SINGLETON_KEY];
|
|
153
|
+
resolve();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
static getInstance() {
|
|
158
|
+
return _global[SINGLETON_KEY];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Register a named check on this HealthzServer instance dynamically.
|
|
162
|
+
*
|
|
163
|
+
* @param check - The named check to register
|
|
164
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
165
|
+
* @returns This instance for chaining
|
|
166
|
+
*/
|
|
167
|
+
registerCheck(check, type = "readiness") {
|
|
168
|
+
if (type === "liveness" || type === "both") {
|
|
169
|
+
this.config.checks.push(check);
|
|
170
|
+
}
|
|
171
|
+
if (type === "readiness" || type === "both") {
|
|
172
|
+
this.config.readinessChecks ??= [
|
|
173
|
+
...this.config.checks
|
|
174
|
+
];
|
|
175
|
+
this.config.readinessChecks.push(check);
|
|
176
|
+
}
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Register a named check on the active singleton HealthzServer instance (if started).
|
|
181
|
+
*
|
|
182
|
+
* @param check - The named check to register
|
|
183
|
+
* @param type - Which probe to attach this check to ('readiness', 'liveness', or 'both')
|
|
184
|
+
*/
|
|
185
|
+
static registerCheck(check, type = "readiness") {
|
|
186
|
+
_global[SINGLETON_KEY]?.registerCheck(check, type);
|
|
187
|
+
}
|
|
188
|
+
async handleRequest(req, res) {
|
|
189
|
+
const isHead = req.method === "HEAD";
|
|
190
|
+
if (req.method !== "GET" && !isHead) {
|
|
191
|
+
this.sendJson(res, 405, {
|
|
192
|
+
error: "Method Not Allowed"
|
|
193
|
+
}, isHead);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const url = req.url ?? "/";
|
|
197
|
+
try {
|
|
198
|
+
if (url === this.config.healthzPath) {
|
|
199
|
+
await this.handleLiveness(res, isHead);
|
|
200
|
+
} else if (url === this.config.readyzPath) {
|
|
201
|
+
await this.handleReadiness(res, isHead);
|
|
202
|
+
} else if (url === this.config.startupzPath) {
|
|
203
|
+
this.handleStartup(res, isHead);
|
|
204
|
+
} else {
|
|
205
|
+
this.sendJson(res, 404, {
|
|
206
|
+
error: "Not Found"
|
|
207
|
+
}, isHead);
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const message = err instanceof Error ? err.message : "Internal Server Error";
|
|
211
|
+
this.sendJson(res, 500, {
|
|
212
|
+
error: message
|
|
213
|
+
}, isHead);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** `/healthz` — Liveness probe */
|
|
217
|
+
async handleLiveness(res, isHead = false) {
|
|
218
|
+
const results = await this.runChecks(this.config.checks);
|
|
219
|
+
if (this.config.onHealthCheck) {
|
|
220
|
+
const start = Date.now();
|
|
221
|
+
try {
|
|
222
|
+
const ok = await this.executeCheck(this.config.onHealthCheck, this.config.checkTimeoutMs);
|
|
223
|
+
results.push({
|
|
224
|
+
name: "custom",
|
|
225
|
+
ok: !!ok,
|
|
226
|
+
durationMs: Date.now() - start,
|
|
227
|
+
...!ok ? {
|
|
228
|
+
error: "Health check returned false"
|
|
229
|
+
} : {}
|
|
230
|
+
});
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
233
|
+
results.push({
|
|
234
|
+
name: "custom",
|
|
235
|
+
ok: false,
|
|
236
|
+
durationMs: Date.now() - start,
|
|
237
|
+
error: message
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const allOk = results.every((r) => r.ok);
|
|
242
|
+
const status = allOk ? "ok" : "unhealthy";
|
|
243
|
+
const httpStatus = allOk ? 200 : 503;
|
|
244
|
+
this.sendProbe(res, httpStatus, status, results, isHead);
|
|
245
|
+
}
|
|
246
|
+
/** `/readyz` — Readiness probe */
|
|
247
|
+
async handleReadiness(res, isHead = false) {
|
|
248
|
+
if (this.shuttingDown || !this.ready) {
|
|
249
|
+
const status2 = "unhealthy";
|
|
250
|
+
this.sendProbe(res, 503, status2, [
|
|
251
|
+
{
|
|
252
|
+
name: "readiness",
|
|
253
|
+
ok: false,
|
|
254
|
+
durationMs: 0,
|
|
255
|
+
error: this.shuttingDown ? "Shutting down" : "Not ready"
|
|
256
|
+
}
|
|
257
|
+
], isHead);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const checksToRun = this.config.readinessChecks ?? this.config.checks;
|
|
261
|
+
const results = await this.runChecks(checksToRun);
|
|
262
|
+
if (this.config.onReadinessCheck) {
|
|
263
|
+
const start = Date.now();
|
|
264
|
+
try {
|
|
265
|
+
const ready = await this.executeCheck(this.config.onReadinessCheck, this.config.checkTimeoutMs);
|
|
266
|
+
results.push({
|
|
267
|
+
name: "custom-readiness",
|
|
268
|
+
ok: !!ready,
|
|
269
|
+
durationMs: Date.now() - start,
|
|
270
|
+
...!ready ? {
|
|
271
|
+
error: "Readiness check returned false"
|
|
272
|
+
} : {}
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
276
|
+
results.push({
|
|
277
|
+
name: "custom-readiness",
|
|
278
|
+
ok: false,
|
|
279
|
+
durationMs: Date.now() - start,
|
|
280
|
+
error: message
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const allOk = results.every((r) => r.ok);
|
|
285
|
+
const status = allOk ? "ok" : "unhealthy";
|
|
286
|
+
const httpStatus = allOk ? 200 : 503;
|
|
287
|
+
this.sendProbe(res, httpStatus, status, results, isHead);
|
|
288
|
+
}
|
|
289
|
+
/** `/startupz` — Startup probe */
|
|
290
|
+
handleStartup(res, isHead = false) {
|
|
291
|
+
if (this.started) {
|
|
292
|
+
this.sendProbe(res, 200, "ok", [], isHead);
|
|
293
|
+
} else {
|
|
294
|
+
this.sendProbe(res, 503, "unhealthy", [
|
|
295
|
+
{
|
|
296
|
+
name: "startup",
|
|
297
|
+
ok: false,
|
|
298
|
+
durationMs: 0,
|
|
299
|
+
error: "Service has not started yet"
|
|
300
|
+
}
|
|
301
|
+
], isHead);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async runChecks(checks) {
|
|
305
|
+
if (checks.length === 0) return [];
|
|
306
|
+
return Promise.all(checks.map(async ({ name, check }) => {
|
|
307
|
+
const start = Date.now();
|
|
308
|
+
try {
|
|
309
|
+
const result = await this.executeCheck(check, this.config.checkTimeoutMs);
|
|
310
|
+
return {
|
|
311
|
+
name,
|
|
312
|
+
ok: !!result,
|
|
313
|
+
durationMs: Date.now() - start
|
|
314
|
+
};
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
317
|
+
return {
|
|
318
|
+
name,
|
|
319
|
+
ok: false,
|
|
320
|
+
durationMs: Date.now() - start,
|
|
321
|
+
error: message
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
sendProbe(res, httpStatus, status, checks, isHead = false) {
|
|
327
|
+
const body = {
|
|
328
|
+
status,
|
|
329
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
330
|
+
uptimeSeconds: Math.floor((Date.now() - this.startedAt) / 1e3)
|
|
331
|
+
};
|
|
332
|
+
if (this.config.detailed && checks.length > 0) {
|
|
333
|
+
body.checks = checks;
|
|
334
|
+
}
|
|
335
|
+
this.sendJson(res, httpStatus, body, isHead);
|
|
336
|
+
}
|
|
337
|
+
sendJson(res, status, body, isHead = false) {
|
|
338
|
+
const payload = JSON.stringify(body);
|
|
339
|
+
res.writeHead(status, {
|
|
340
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
341
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
342
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
343
|
+
"X-Content-Type-Options": "nosniff"
|
|
344
|
+
});
|
|
345
|
+
if (isHead) {
|
|
346
|
+
res.end();
|
|
347
|
+
} else {
|
|
348
|
+
res.end(payload);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async executeCheck(fn, ms) {
|
|
352
|
+
const controller = new AbortController();
|
|
353
|
+
let timer;
|
|
354
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
355
|
+
timer = setTimeout(() => {
|
|
356
|
+
const error = new Error(`Check timed out after ${ms}ms`);
|
|
357
|
+
controller.abort(error);
|
|
358
|
+
reject(error);
|
|
359
|
+
}, ms);
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
const checkPromise = Promise.resolve().then(() => fn(controller.signal));
|
|
363
|
+
checkPromise.catch(() => {
|
|
364
|
+
});
|
|
365
|
+
return await Promise.race([
|
|
366
|
+
checkPromise,
|
|
367
|
+
timeoutPromise
|
|
368
|
+
]);
|
|
369
|
+
} finally {
|
|
370
|
+
if (timer) clearTimeout(timer);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async initiateShutdown() {
|
|
374
|
+
if (this.shuttingDown) return;
|
|
375
|
+
this.shuttingDown = true;
|
|
376
|
+
this.ready = false;
|
|
377
|
+
if (this.config.shutdownDelayMs > 0) {
|
|
378
|
+
await new Promise((r) => setTimeout(r, this.config.shutdownDelayMs));
|
|
379
|
+
}
|
|
380
|
+
await _HealthzServer.stop();
|
|
381
|
+
}
|
|
382
|
+
cleanup() {
|
|
383
|
+
process.off("SIGTERM", this.onSigterm);
|
|
384
|
+
process.off("SIGINT", this.onSigint);
|
|
385
|
+
this.ready = false;
|
|
386
|
+
this.started = false;
|
|
387
|
+
this.shuttingDown = false;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
exports.HealthzServer = HealthzServer;
|
|
392
|
+
exports.getDefaultHealthzConfig = getDefaultHealthzConfig;
|
|
393
|
+
exports.resolveConfig = resolveConfig;
|