@adalo/metrics 0.1.121 → 0.1.123
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/lib/healthCheckClient.d.ts +11 -7
- package/lib/healthCheckClient.d.ts.map +1 -1
- package/lib/healthCheckClient.js +147 -41
- package/lib/healthCheckClient.js.map +1 -1
- package/package.json +1 -1
- package/src/healthCheckClient.js +140 -41
|
@@ -136,8 +136,10 @@ export class HealthCheckClient {
|
|
|
136
136
|
_cachedResult: CachedHealthResult | null;
|
|
137
137
|
/** @type {Map<string, Pool>} */
|
|
138
138
|
_databasePools: Map<string, Pool>;
|
|
139
|
+
/** @type {DatabaseConfig | null} */
|
|
140
|
+
_mainDatabaseConfig: DatabaseConfig | null;
|
|
139
141
|
/** @type {DatabaseConfig[]} */
|
|
140
|
-
|
|
142
|
+
_clusterConfigs: DatabaseConfig[];
|
|
141
143
|
_redisClientType: string | undefined;
|
|
142
144
|
/**
|
|
143
145
|
* Initialize database configurations from options.
|
|
@@ -166,8 +168,8 @@ export class HealthCheckClient {
|
|
|
166
168
|
*/
|
|
167
169
|
private _checkSingleDatabase;
|
|
168
170
|
/**
|
|
169
|
-
* Tests all PostgreSQL
|
|
170
|
-
* @returns {Promise<
|
|
171
|
+
* Tests all PostgreSQL databases (main + clusters) in parallel.
|
|
172
|
+
* @returns {Promise<Object | null>} Database health with optional clusters
|
|
171
173
|
* @private
|
|
172
174
|
*/
|
|
173
175
|
private _checkAllDatabases;
|
|
@@ -190,15 +192,17 @@ export class HealthCheckClient {
|
|
|
190
192
|
clearCache(): void;
|
|
191
193
|
/**
|
|
192
194
|
* Builds a list of error messages from health check result.
|
|
195
|
+
* All error messages are sanitized to remove sensitive information.
|
|
193
196
|
* @param {HealthCheckResult} result - Health check result
|
|
194
|
-
* @returns {string[]} Array of error messages
|
|
197
|
+
* @returns {string[]} Array of sanitized error messages
|
|
195
198
|
* @private
|
|
196
199
|
*/
|
|
197
200
|
private _getErrorMessages;
|
|
198
201
|
/**
|
|
199
202
|
* Express middleware handler for health check endpoint.
|
|
200
|
-
* Returns 200
|
|
201
|
-
*
|
|
203
|
+
* Returns 200 for healthy/degraded, 503 for unhealthy.
|
|
204
|
+
* Response includes errors array when status is not healthy.
|
|
205
|
+
* All sensitive data (passwords, connection strings, etc.) is masked.
|
|
202
206
|
*
|
|
203
207
|
* @returns {(req: any, res: any) => Promise<void>} Express request handler
|
|
204
208
|
*/
|
|
@@ -207,7 +211,7 @@ export class HealthCheckClient {
|
|
|
207
211
|
* Register health check endpoint on an Express app.
|
|
208
212
|
*
|
|
209
213
|
* @param {import('express').Application} app - Express application
|
|
210
|
-
* @param {string} [path='/health'] - Path for the health endpoint
|
|
214
|
+
* @param {string} [path='/health-status'] - Path for the health endpoint
|
|
211
215
|
*/
|
|
212
216
|
registerHealthEndpoint(app: any, path?: string | undefined): void;
|
|
213
217
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../src/healthCheckClient.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../src/healthCheckClient.js"],"names":[],"mappings":"2BAgEa,SAAS,GAAG,WAAW,GAAG,UAAU;;;;;YAKnC,YAAY;;;;;;;;;;;;;;YAOZ,YAAY;;;;;YACL,MAAM,GAAE,eAAe;;;;;;;YAK9B,YAAY;;;;eACZ,MAAM;;;;YACN,OAAO;;;;;YACA,MAAM,GAAE,eAAe,GAAG,qBAAqB;;;;;;;YAKtD,iBAAiB;;;;eACjB,MAAM;;;;;;UAKN,MAAM;;;;SACN,MAAM;;AAlCpB;;GAEG;AAEH;;;;;GAKG;AAEH;;;;GAIG;AAEH;;;;;;GAMG;AAEH;;;;GAIG;AAEH;;;;GAIG;AAEH;;;;;;;;;;;GAWG;AACH;IACE;;;;;;;;OAQG;IACH;QAP4B,WAAW;QACX,YAAY;QACI,sBAAsB;;;QACzC,WAAW,GAAzB,GAAG;QACc,UAAU;QACV,OAAO;OA2BlC;IAxBC,iBAA8C;IAC9C,mBAAiE;IACjE,gBACgE;IAEhE,mBAAmD;IAEnD,wCAAwC;IACxC,eADW,kBAAkB,GAAG,IAAI,CACX;IAEzB,gCAAgC;IAChC,gBADW,IAAI,MAAM,EAAE,IAAI,CAAC,CACG;IAE/B,oCAAoC;IACpC,qBADW,cAAc,GAAG,IAAI,CACD;IAE/B,+BAA+B;IAC/B,iBADW,cAAc,EAAE,CACF;IAKvB,qCAA4D;IAIhE;;;;OAIG;IACH,uBAcC;IAED;;;;;OAKG;IACH,iBAaC;IAED;;;;OAIG;IACH,sBAGC;IAED;;;;;OAKG;IACH,6BAiBC;IAED;;;;OAIG;IACH,2BAoDC;IAED;;;;OAIG;IACH,oBA6CC;IAED;;;;;OAKG;IACH,sBAFa,QAAQ,iBAAiB,CAAC,CAuCtC;IAED;;OAEG;IACH,mBAEC;IAED;;;;;;OAMG;IACH,0BA6BC;IAED;;;;;;;OAOG;IACH,uBAFmB,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,CA4BjD;IAED;;;;;OAKG;IACH,kEAGC;IAED;;;OAGG;IACH,WAFa,QAAQ,IAAI,CAAC,CAWzB;CACF"}
|
package/lib/healthCheckClient.js
CHANGED
|
@@ -11,6 +11,60 @@ const {
|
|
|
11
11
|
} = require('./redisUtils');
|
|
12
12
|
const HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Patterns to detect and mask sensitive information in error messages
|
|
16
|
+
*/
|
|
17
|
+
const SENSITIVE_PATTERNS = [
|
|
18
|
+
// Database connection strings: postgres://user:password@host:port/database
|
|
19
|
+
{
|
|
20
|
+
pattern: /(postgres(?:ql)?|mysql|mongodb|redis|amqp):\/\/([^:]+):([^@]+)@([^:/]+)(:\d+)?\/([^\s?]+)/gi,
|
|
21
|
+
replacement: '$1://***:***@***$5/***'
|
|
22
|
+
},
|
|
23
|
+
// Generic URLs with credentials: protocol://user:password@host
|
|
24
|
+
{
|
|
25
|
+
pattern: /(\w+):\/\/([^:]+):([^@]+)@([^\s/]+)/gi,
|
|
26
|
+
replacement: '$1://***:***@***'
|
|
27
|
+
},
|
|
28
|
+
// Password fields in JSON or key=value format
|
|
29
|
+
{
|
|
30
|
+
pattern: /(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
31
|
+
replacement: '$1=***'
|
|
32
|
+
},
|
|
33
|
+
// Database/table/schema/role/user names in error messages: database "name", table "name", etc.
|
|
34
|
+
{
|
|
35
|
+
pattern: /(database|table|schema|role|user|relation|column|index)\s*["']([^"']+)["']/gi,
|
|
36
|
+
replacement: '$1 "***"'
|
|
37
|
+
},
|
|
38
|
+
// IP addresses (to hide internal network structure)
|
|
39
|
+
{
|
|
40
|
+
pattern: /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?\b/g,
|
|
41
|
+
replacement: '***$2'
|
|
42
|
+
},
|
|
43
|
+
// Host names that might reveal internal infrastructure
|
|
44
|
+
{
|
|
45
|
+
pattern: /\b(host|hostname|server)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
46
|
+
replacement: '$1=***'
|
|
47
|
+
}];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Masks sensitive information in a string
|
|
51
|
+
* @param {string} text - Text that might contain sensitive data
|
|
52
|
+
* @returns {string} - Text with sensitive data masked
|
|
53
|
+
*/
|
|
54
|
+
function maskSensitiveData(text) {
|
|
55
|
+
if (!text || typeof text !== 'string') {
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
let masked = text;
|
|
59
|
+
for (const {
|
|
60
|
+
pattern,
|
|
61
|
+
replacement
|
|
62
|
+
} of SENSITIVE_PATTERNS) {
|
|
63
|
+
masked = masked.replace(pattern, replacement);
|
|
64
|
+
}
|
|
65
|
+
return masked;
|
|
66
|
+
}
|
|
67
|
+
|
|
14
68
|
/**
|
|
15
69
|
* @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus
|
|
16
70
|
*/
|
|
@@ -82,8 +136,11 @@ class HealthCheckClient {
|
|
|
82
136
|
/** @type {Map<string, Pool>} */
|
|
83
137
|
this._databasePools = new Map();
|
|
84
138
|
|
|
139
|
+
/** @type {DatabaseConfig | null} */
|
|
140
|
+
this._mainDatabaseConfig = null;
|
|
141
|
+
|
|
85
142
|
/** @type {DatabaseConfig[]} */
|
|
86
|
-
this.
|
|
143
|
+
this._clusterConfigs = [];
|
|
87
144
|
this._initDatabases(options);
|
|
88
145
|
if (this.redisClient) {
|
|
89
146
|
this._redisClientType = getRedisClientType(this.redisClient);
|
|
@@ -97,17 +154,17 @@ class HealthCheckClient {
|
|
|
97
154
|
*/
|
|
98
155
|
_initDatabases(options) {
|
|
99
156
|
const mainUrl = options.databaseUrl || process.env.DATABASE_URL || '';
|
|
100
|
-
const mainName = options.databaseName ||
|
|
157
|
+
const mainName = options.databaseName || `${this.appName}_db`;
|
|
101
158
|
if (mainUrl) {
|
|
102
|
-
this.
|
|
159
|
+
this._mainDatabaseConfig = {
|
|
103
160
|
name: mainName,
|
|
104
161
|
url: mainUrl
|
|
105
|
-
}
|
|
162
|
+
};
|
|
106
163
|
}
|
|
107
164
|
const additionalUrls = options.additionalDatabaseUrls || {};
|
|
108
165
|
for (const [name, url] of Object.entries(additionalUrls)) {
|
|
109
166
|
if (url) {
|
|
110
|
-
this.
|
|
167
|
+
this._clusterConfigs.push({
|
|
111
168
|
name,
|
|
112
169
|
url
|
|
113
170
|
});
|
|
@@ -161,43 +218,69 @@ class HealthCheckClient {
|
|
|
161
218
|
} catch (err) {
|
|
162
219
|
return {
|
|
163
220
|
status: 'unhealthy',
|
|
164
|
-
message: err.message,
|
|
221
|
+
message: maskSensitiveData(err.message),
|
|
165
222
|
latencyMs: Date.now() - start
|
|
166
223
|
};
|
|
167
224
|
}
|
|
168
225
|
}
|
|
169
226
|
|
|
170
227
|
/**
|
|
171
|
-
* Tests all PostgreSQL
|
|
172
|
-
* @returns {Promise<
|
|
228
|
+
* Tests all PostgreSQL databases (main + clusters) in parallel.
|
|
229
|
+
* @returns {Promise<Object | null>} Database health with optional clusters
|
|
173
230
|
* @private
|
|
174
231
|
*/
|
|
175
232
|
async _checkAllDatabases() {
|
|
176
|
-
if (this.
|
|
233
|
+
if (!this._mainDatabaseConfig && this._clusterConfigs.length === 0) {
|
|
177
234
|
return null;
|
|
178
235
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
236
|
+
|
|
237
|
+
// Check main database
|
|
238
|
+
let mainHealth = null;
|
|
239
|
+
if (this._mainDatabaseConfig) {
|
|
240
|
+
mainHealth = await this._checkSingleDatabase(this._mainDatabaseConfig);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check clusters in parallel
|
|
244
|
+
let clusters = null;
|
|
245
|
+
if (this._clusterConfigs.length > 0) {
|
|
246
|
+
const clusterResults = await Promise.all(this._clusterConfigs.map(async config => ({
|
|
247
|
+
name: config.name,
|
|
248
|
+
health: await this._checkSingleDatabase(config)
|
|
249
|
+
})));
|
|
250
|
+
clusters = {};
|
|
251
|
+
for (const {
|
|
252
|
+
name,
|
|
253
|
+
health
|
|
254
|
+
} of clusterResults) {
|
|
255
|
+
clusters[name] = health;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Calculate overall status
|
|
260
|
+
const allStatuses = [];
|
|
261
|
+
if (mainHealth) allStatuses.push(mainHealth.status);
|
|
262
|
+
if (clusters) {
|
|
263
|
+
allStatuses.push(...Object.values(clusters).map(c => c.status));
|
|
189
264
|
}
|
|
190
|
-
const statuses = Object.values(clusters).map(c => c.status);
|
|
191
265
|
let overallStatus = 'healthy';
|
|
192
|
-
if (
|
|
266
|
+
if (allStatuses.some(s => s === 'unhealthy')) {
|
|
193
267
|
overallStatus = 'unhealthy';
|
|
194
|
-
} else if (
|
|
268
|
+
} else if (allStatuses.some(s => s === 'degraded')) {
|
|
195
269
|
overallStatus = 'degraded';
|
|
196
270
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
271
|
+
|
|
272
|
+
// Build result
|
|
273
|
+
const result = {
|
|
274
|
+
status: overallStatus
|
|
200
275
|
};
|
|
276
|
+
if (mainHealth) {
|
|
277
|
+
result.latencyMs = mainHealth.latencyMs;
|
|
278
|
+
if (mainHealth.message) result.message = mainHealth.message;
|
|
279
|
+
}
|
|
280
|
+
if (clusters) {
|
|
281
|
+
result.clusters = clusters;
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
201
284
|
}
|
|
202
285
|
|
|
203
286
|
/**
|
|
@@ -237,13 +320,13 @@ class HealthCheckClient {
|
|
|
237
320
|
}
|
|
238
321
|
return {
|
|
239
322
|
status: 'unhealthy',
|
|
240
|
-
message: `Unexpected PING response: ${pong}
|
|
323
|
+
message: maskSensitiveData(`Unexpected PING response: ${pong}`),
|
|
241
324
|
latencyMs: Date.now() - start
|
|
242
325
|
};
|
|
243
326
|
} catch (err) {
|
|
244
327
|
return {
|
|
245
328
|
status: 'unhealthy',
|
|
246
|
-
message: err.message,
|
|
329
|
+
message: maskSensitiveData(err.message),
|
|
247
330
|
latencyMs: Date.now() - start
|
|
248
331
|
};
|
|
249
332
|
}
|
|
@@ -297,32 +380,44 @@ class HealthCheckClient {
|
|
|
297
380
|
|
|
298
381
|
/**
|
|
299
382
|
* Builds a list of error messages from health check result.
|
|
383
|
+
* All error messages are sanitized to remove sensitive information.
|
|
300
384
|
* @param {HealthCheckResult} result - Health check result
|
|
301
|
-
* @returns {string[]} Array of error messages
|
|
385
|
+
* @returns {string[]} Array of sanitized error messages
|
|
302
386
|
* @private
|
|
303
387
|
*/
|
|
304
388
|
_getErrorMessages(result) {
|
|
305
389
|
const errors = [];
|
|
306
390
|
if (result.components.database) {
|
|
307
391
|
const db = result.components.database;
|
|
392
|
+
|
|
393
|
+
// Check main database status
|
|
394
|
+
if (db.status === 'unhealthy' && db.message) {
|
|
395
|
+
const dbName = this._mainDatabaseConfig?.name || 'main';
|
|
396
|
+
errors.push(`DB ${dbName}: ${maskSensitiveData(db.message)}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check clusters
|
|
308
400
|
if (db.clusters) {
|
|
309
401
|
for (const [name, health] of Object.entries(db.clusters)) {
|
|
310
402
|
if (health.status === 'unhealthy') {
|
|
311
|
-
|
|
403
|
+
const message = health.message || 'connection failed';
|
|
404
|
+
errors.push(`DB ${name}: ${maskSensitiveData(message)}`);
|
|
312
405
|
}
|
|
313
406
|
}
|
|
314
407
|
}
|
|
315
408
|
}
|
|
316
409
|
if (result.components.redis && result.components.redis.status === 'unhealthy') {
|
|
317
|
-
|
|
410
|
+
const message = result.components.redis.message || 'connection failed';
|
|
411
|
+
errors.push(`Redis: ${maskSensitiveData(message)}`);
|
|
318
412
|
}
|
|
319
413
|
return errors;
|
|
320
414
|
}
|
|
321
415
|
|
|
322
416
|
/**
|
|
323
417
|
* Express middleware handler for health check endpoint.
|
|
324
|
-
* Returns 200
|
|
325
|
-
*
|
|
418
|
+
* Returns 200 for healthy/degraded, 503 for unhealthy.
|
|
419
|
+
* Response includes errors array when status is not healthy.
|
|
420
|
+
* All sensitive data (passwords, connection strings, etc.) is masked.
|
|
326
421
|
*
|
|
327
422
|
* @returns {(req: any, res: any) => Promise<void>} Express request handler
|
|
328
423
|
*/
|
|
@@ -330,16 +425,27 @@ class HealthCheckClient {
|
|
|
330
425
|
return async (req, res) => {
|
|
331
426
|
try {
|
|
332
427
|
const result = await this.performHealthCheck();
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
428
|
+
const statusCode = result.status === 'unhealthy' ? 503 : 200;
|
|
429
|
+
|
|
430
|
+
// Build response with errors if not healthy
|
|
431
|
+
const response = {
|
|
432
|
+
...result
|
|
433
|
+
};
|
|
434
|
+
if (result.status !== 'healthy') {
|
|
435
|
+
const errors = this._getErrorMessages(result);
|
|
436
|
+
if (errors.length > 0) {
|
|
437
|
+
response.errors = errors;
|
|
438
|
+
}
|
|
336
439
|
}
|
|
337
|
-
|
|
338
|
-
const errorMessage = errors.length > 0 ? `UNHEALTHY: ${errors.join('; ')}` : 'UNHEALTHY: Unknown error';
|
|
339
|
-
res.status(503).set('Content-Type', 'text/plain').send(errorMessage);
|
|
440
|
+
res.status(statusCode).json(response);
|
|
340
441
|
} catch (err) {
|
|
341
442
|
console.error(`${this.prefixLogs} Health check failed:`, err);
|
|
342
|
-
res.status(503).
|
|
443
|
+
res.status(503).json({
|
|
444
|
+
status: 'unhealthy',
|
|
445
|
+
timestamp: new Date().toISOString(),
|
|
446
|
+
cached: false,
|
|
447
|
+
errors: [maskSensitiveData(err.message)]
|
|
448
|
+
});
|
|
343
449
|
}
|
|
344
450
|
};
|
|
345
451
|
}
|
|
@@ -348,9 +454,9 @@ class HealthCheckClient {
|
|
|
348
454
|
* Register health check endpoint on an Express app.
|
|
349
455
|
*
|
|
350
456
|
* @param {import('express').Application} app - Express application
|
|
351
|
-
* @param {string} [path='/health'] - Path for the health endpoint
|
|
457
|
+
* @param {string} [path='/health-status'] - Path for the health endpoint
|
|
352
458
|
*/
|
|
353
|
-
registerHealthEndpoint(app, path = '/health') {
|
|
459
|
+
registerHealthEndpoint(app, path = '/health-status') {
|
|
354
460
|
app.get(path, this.healthHandler());
|
|
355
461
|
console.info(`${this.prefixLogs} Registered health endpoint at ${path}`);
|
|
356
462
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckClient.js","names":["Pool","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","HEALTH_CHECK_CACHE_TTL_MS","HealthCheckClient","constructor","options","redisClient","cacheTtlMs","appName","process","env","BUILD_APP_NAME","prefixLogs","_cachedResult","_databasePools","Map","_databaseConfigs","_initDatabases","_redisClientType","mainUrl","databaseUrl","DATABASE_URL","mainName","databaseName","push","name","url","additionalUrls","additionalDatabaseUrls","Object","entries","_getPool","config","has","set","connectionString","max","idleTimeoutMillis","connectionTimeoutMillis","get","_isCacheValid","Date","now","timestamp","_checkSingleDatabase","start","pool","query","status","latencyMs","err","message","_checkAllDatabases","length","results","Promise","all","map","health","clusters","statuses","values","c","overallStatus","some","s","_checkRedis","pong","resolve","reject","ping","result","performHealthCheck","cached","dbHealth","redisHealth","components","database","redis","toISOString","clearCache","_getErrorMessages","errors","db","healthHandler","req","res","send","errorMessage","join","console","error","registerHealthEndpoint","app","path","info","cleanup","end","clear","module","exports"],"sources":["../src/healthCheckClient.js"],"sourcesContent":["const { Pool } = require('pg')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('./redisUtils')\n\nconst HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000\n\n/**\n * @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus\n */\n\n/**\n * @typedef {Object} ComponentHealth\n * @property {HealthStatus} status - Component health status\n * @property {string} [message] - Optional status message\n * @property {number} [latencyMs] - Connection latency in milliseconds\n */\n\n/**\n * @typedef {Object} DatabaseClusterHealth\n * @property {HealthStatus} status - Overall databases status\n * @property {Object<string, ComponentHealth>} clusters - Individual cluster health\n */\n\n/**\n * @typedef {Object} HealthCheckResult\n * @property {HealthStatus} status - Overall health status\n * @property {string} timestamp - ISO timestamp of the check\n * @property {boolean} cached - Whether this result is from cache\n * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health\n */\n\n/**\n * @typedef {Object} CachedHealthResult\n * @property {HealthCheckResult} result - The cached health check result\n * @property {number} timestamp - Unix timestamp when cached\n */\n\n/**\n * @typedef {Object} DatabaseConfig\n * @property {string} name - Database/cluster name\n * @property {string} url - Connection URL\n */\n\n/**\n * HealthCheckClient provides a health check middleware for external monitoring services\n * like BetterStack. It validates database and Redis connections with rate limiting\n * to prevent excessive load on backend services.\n *\n * Features:\n * - Multi-cluster DB validation (PostgreSQL)\n * - Redis connection validation (supports ioredis, node-redis v3/v4)\n * - Result caching (default: 60 seconds) to prevent overloading services\n * - Express middleware support\n * - BetterStack-compatible JSON response format\n */\nclass HealthCheckClient {\n /**\n * @param {Object} options\n * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL\n * @param {string} [options.databaseName='main'] - Name for the main database\n * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)\n * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)\n * @param {string} [options.appName] - Application name for logging\n */\n constructor(options = {}) {\n this.redisClient = options.redisClient || null\n this.cacheTtlMs = options.cacheTtlMs ?? HEALTH_CHECK_CACHE_TTL_MS\n this.appName =\n options.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n\n this.prefixLogs = `[${this.appName}] [HealthCheck]`\n\n /** @type {CachedHealthResult | null} */\n this._cachedResult = null\n\n /** @type {Map<string, Pool>} */\n this._databasePools = new Map()\n\n /** @type {DatabaseConfig[]} */\n this._databaseConfigs = []\n\n this._initDatabases(options)\n\n if (this.redisClient) {\n this._redisClientType = getRedisClientType(this.redisClient)\n }\n }\n\n /**\n * Initialize database configurations from options.\n * @param {Object} options - Constructor options\n * @private\n */\n _initDatabases(options) {\n const mainUrl = options.databaseUrl || process.env.DATABASE_URL || ''\n const mainName = options.databaseName || 'main'\n\n if (mainUrl) {\n this._databaseConfigs.push({ name: mainName, url: mainUrl })\n }\n\n const additionalUrls = options.additionalDatabaseUrls || {}\n for (const [name, url] of Object.entries(additionalUrls)) {\n if (url) {\n this._databaseConfigs.push({ name, url })\n }\n }\n }\n\n /**\n * Get or create a database pool for a given config.\n * @param {DatabaseConfig} config - Database configuration\n * @returns {Pool}\n * @private\n */\n _getPool(config) {\n if (!this._databasePools.has(config.name)) {\n this._databasePools.set(\n config.name,\n new Pool({\n connectionString: config.url,\n max: 1,\n idleTimeoutMillis: 30000,\n connectionTimeoutMillis: 5000,\n })\n )\n }\n return this._databasePools.get(config.name)\n }\n\n /**\n * Checks if cached result is still valid based on TTL.\n * @returns {boolean}\n * @private\n */\n _isCacheValid() {\n if (!this._cachedResult) return false\n return Date.now() - this._cachedResult.timestamp < this.cacheTtlMs\n }\n\n /**\n * Tests a single database cluster connectivity.\n * @param {DatabaseConfig} config - Database configuration\n * @returns {Promise<ComponentHealth>}\n * @private\n */\n async _checkSingleDatabase(config) {\n const start = Date.now()\n\n try {\n const pool = this._getPool(config)\n await pool.query('SELECT 1')\n return {\n status: 'healthy',\n latencyMs: Date.now() - start,\n }\n } catch (err) {\n return {\n status: 'unhealthy',\n message: err.message,\n latencyMs: Date.now() - start,\n }\n }\n }\n\n /**\n * Tests all PostgreSQL database clusters in parallel.\n * @returns {Promise<DatabaseClusterHealth | null>}\n * @private\n */\n async _checkAllDatabases() {\n if (this._databaseConfigs.length === 0) {\n return null\n }\n\n const results = await Promise.all(\n this._databaseConfigs.map(async config => ({\n name: config.name,\n health: await this._checkSingleDatabase(config),\n }))\n )\n\n const clusters = {}\n for (const { name, health } of results) {\n clusters[name] = health\n }\n\n const statuses = Object.values(clusters).map(c => c.status)\n let overallStatus = 'healthy'\n if (statuses.some(s => s === 'unhealthy')) {\n overallStatus = 'unhealthy'\n } else if (statuses.some(s => s === 'degraded')) {\n overallStatus = 'degraded'\n }\n\n return { status: overallStatus, clusters }\n }\n\n /**\n * Tests Redis connectivity using PING command.\n * @returns {Promise<ComponentHealth>}\n * @private\n */\n async _checkRedis() {\n if (!this.redisClient) {\n return { status: 'healthy', message: 'Not configured' }\n }\n\n const start = Date.now()\n\n try {\n let pong\n\n if (this._redisClientType === REDIS_V3) {\n pong = await new Promise((resolve, reject) => {\n this.redisClient.ping((err, result) => {\n if (err) reject(err)\n else resolve(result)\n })\n })\n } else if (\n this._redisClientType === REDIS_V4 ||\n this._redisClientType === IOREDIS\n ) {\n pong = await this.redisClient.ping()\n } else {\n return { status: 'unhealthy', message: 'Unknown Redis client type' }\n }\n\n if (pong === 'PONG') {\n return {\n status: 'healthy',\n latencyMs: Date.now() - start,\n }\n }\n\n return {\n status: 'unhealthy',\n message: `Unexpected PING response: ${pong}`,\n latencyMs: Date.now() - start,\n }\n } catch (err) {\n return {\n status: 'unhealthy',\n message: err.message,\n latencyMs: Date.now() - start,\n }\n }\n }\n\n /**\n * Performs a full health check on all configured components.\n * Results are cached for the configured TTL to prevent excessive load.\n *\n * @returns {Promise<HealthCheckResult>}\n */\n async performHealthCheck() {\n if (this._isCacheValid()) {\n return { ...this._cachedResult.result, cached: true }\n }\n\n const [dbHealth, redisHealth] = await Promise.all([\n this._checkAllDatabases(),\n this._checkRedis(),\n ])\n\n const components = {}\n if (dbHealth) components.database = dbHealth\n if (this.redisClient) components.redis = redisHealth\n\n const statuses = Object.values(components).map(c => c.status)\n let overallStatus = 'healthy'\n\n if (statuses.some(s => s === 'unhealthy')) {\n overallStatus = 'unhealthy'\n } else if (statuses.some(s => s === 'degraded')) {\n overallStatus = 'degraded'\n }\n\n /** @type {HealthCheckResult} */\n const result = {\n status: overallStatus,\n timestamp: new Date().toISOString(),\n cached: false,\n components,\n }\n\n this._cachedResult = {\n result,\n timestamp: Date.now(),\n }\n\n return result\n }\n\n /**\n * Clears the cached health check result, forcing the next check to be fresh.\n */\n clearCache() {\n this._cachedResult = null\n }\n\n /**\n * Builds a list of error messages from health check result.\n * @param {HealthCheckResult} result - Health check result\n * @returns {string[]} Array of error messages\n * @private\n */\n _getErrorMessages(result) {\n const errors = []\n\n if (result.components.database) {\n const db = result.components.database\n if (db.clusters) {\n for (const [name, health] of Object.entries(db.clusters)) {\n if (health.status === 'unhealthy') {\n errors.push(`DB ${name}: ${health.message || 'connection failed'}`)\n }\n }\n }\n }\n\n if (result.components.redis && result.components.redis.status === 'unhealthy') {\n errors.push(`Redis: ${result.components.redis.message || 'connection failed'}`)\n }\n\n return errors\n }\n\n /**\n * Express middleware handler for health check endpoint.\n * Returns 200 with \"OK\" for healthy, 503 with error details for unhealthy.\n * Uses plain text response for BetterStack compatibility.\n *\n * @returns {(req: any, res: any) => Promise<void>} Express request handler\n */\n healthHandler() {\n return async (req, res) => {\n try {\n const result = await this.performHealthCheck()\n\n if (result.status === 'healthy') {\n res.status(200).set('Content-Type', 'text/plain').send('OK')\n return\n }\n\n const errors = this._getErrorMessages(result)\n const errorMessage = errors.length > 0\n ? `UNHEALTHY: ${errors.join('; ')}`\n : 'UNHEALTHY: Unknown error'\n\n res.status(503).set('Content-Type', 'text/plain').send(errorMessage)\n } catch (err) {\n console.error(`${this.prefixLogs} Health check failed:`, err)\n res.status(503).set('Content-Type', 'text/plain').send(`UNHEALTHY: ${err.message}`)\n }\n }\n }\n\n /**\n * Register health check endpoint on an Express app.\n *\n * @param {import('express').Application} app - Express application\n * @param {string} [path='/health'] - Path for the health endpoint\n */\n registerHealthEndpoint(app, path = '/health') {\n app.get(path, this.healthHandler())\n console.info(`${this.prefixLogs} Registered health endpoint at ${path}`)\n }\n\n /**\n * Cleanup resources (database pools).\n * @returns {Promise<void>}\n */\n async cleanup() {\n for (const [name, pool] of this._databasePools) {\n try {\n await pool.end()\n } catch (err) {\n console.error(`${this.prefixLogs} Error closing database pool ${name}:`, err)\n }\n }\n this._databasePools.clear()\n }\n}\n\nmodule.exports = { HealthCheckClient }\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"healthCheckClient.js","names":["Pool","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","HEALTH_CHECK_CACHE_TTL_MS","SENSITIVE_PATTERNS","pattern","replacement","maskSensitiveData","text","masked","replace","HealthCheckClient","constructor","options","redisClient","cacheTtlMs","appName","process","env","BUILD_APP_NAME","prefixLogs","_cachedResult","_databasePools","Map","_mainDatabaseConfig","_clusterConfigs","_initDatabases","_redisClientType","mainUrl","databaseUrl","DATABASE_URL","mainName","databaseName","name","url","additionalUrls","additionalDatabaseUrls","Object","entries","push","_getPool","config","has","set","connectionString","max","idleTimeoutMillis","connectionTimeoutMillis","get","_isCacheValid","Date","now","timestamp","_checkSingleDatabase","start","pool","query","status","latencyMs","err","message","_checkAllDatabases","length","mainHealth","clusters","clusterResults","Promise","all","map","health","allStatuses","values","c","overallStatus","some","s","result","_checkRedis","pong","resolve","reject","ping","performHealthCheck","cached","dbHealth","redisHealth","components","database","redis","statuses","toISOString","clearCache","_getErrorMessages","errors","db","dbName","healthHandler","req","res","statusCode","response","json","console","error","registerHealthEndpoint","app","path","info","cleanup","end","clear","module","exports"],"sources":["../src/healthCheckClient.js"],"sourcesContent":["const { Pool } = require('pg')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('./redisUtils')\n\nconst HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000\n\n/**\n * Patterns to detect and mask sensitive information in error messages\n */\nconst SENSITIVE_PATTERNS = [\n // Database connection strings: postgres://user:password@host:port/database\n {\n pattern: /(postgres(?:ql)?|mysql|mongodb|redis|amqp):\\/\\/([^:]+):([^@]+)@([^:/]+)(:\\d+)?\\/([^\\s?]+)/gi,\n replacement: '$1://***:***@***$5/***',\n },\n // Generic URLs with credentials: protocol://user:password@host\n {\n pattern: /(\\w+):\\/\\/([^:]+):([^@]+)@([^\\s/]+)/gi,\n replacement: '$1://***:***@***',\n },\n // Password fields in JSON or key=value format\n {\n pattern: /(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)[\"\\s]*[:=][\"\\s]*([^\\s,}\"]+)/gi,\n replacement: '$1=***',\n },\n // Database/table/schema/role/user names in error messages: database \"name\", table \"name\", etc.\n {\n pattern: /(database|table|schema|role|user|relation|column|index)\\s*[\"']([^\"']+)[\"']/gi,\n replacement: '$1 \"***\"',\n },\n // IP addresses (to hide internal network structure)\n {\n pattern: /\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d+)?\\b/g,\n replacement: '***$2',\n },\n // Host names that might reveal internal infrastructure\n {\n pattern: /\\b(host|hostname|server)[\"\\s]*[:=][\"\\s]*([^\\s,}\"]+)/gi,\n replacement: '$1=***',\n },\n]\n\n/**\n * Masks sensitive information in a string\n * @param {string} text - Text that might contain sensitive data\n * @returns {string} - Text with sensitive data masked\n */\nfunction maskSensitiveData(text) {\n if (!text || typeof text !== 'string') {\n return text\n }\n\n let masked = text\n for (const { pattern, replacement } of SENSITIVE_PATTERNS) {\n masked = masked.replace(pattern, replacement)\n }\n return masked\n}\n\n/**\n * @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus\n */\n\n/**\n * @typedef {Object} ComponentHealth\n * @property {HealthStatus} status - Component health status\n * @property {string} [message] - Optional status message\n * @property {number} [latencyMs] - Connection latency in milliseconds\n */\n\n/**\n * @typedef {Object} DatabaseClusterHealth\n * @property {HealthStatus} status - Overall databases status\n * @property {Object<string, ComponentHealth>} clusters - Individual cluster health\n */\n\n/**\n * @typedef {Object} HealthCheckResult\n * @property {HealthStatus} status - Overall health status\n * @property {string} timestamp - ISO timestamp of the check\n * @property {boolean} cached - Whether this result is from cache\n * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health\n */\n\n/**\n * @typedef {Object} CachedHealthResult\n * @property {HealthCheckResult} result - The cached health check result\n * @property {number} timestamp - Unix timestamp when cached\n */\n\n/**\n * @typedef {Object} DatabaseConfig\n * @property {string} name - Database/cluster name\n * @property {string} url - Connection URL\n */\n\n/**\n * HealthCheckClient provides a health check middleware for external monitoring services\n * like BetterStack. It validates database and Redis connections with rate limiting\n * to prevent excessive load on backend services.\n *\n * Features:\n * - Multi-cluster DB validation (PostgreSQL)\n * - Redis connection validation (supports ioredis, node-redis v3/v4)\n * - Result caching (default: 60 seconds) to prevent overloading services\n * - Express middleware support\n * - BetterStack-compatible JSON response format\n */\nclass HealthCheckClient {\n /**\n * @param {Object} options\n * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL\n * @param {string} [options.databaseName='main'] - Name for the main database\n * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)\n * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)\n * @param {string} [options.appName] - Application name for logging\n */\n constructor(options = {}) {\n this.redisClient = options.redisClient || null\n this.cacheTtlMs = options.cacheTtlMs ?? HEALTH_CHECK_CACHE_TTL_MS\n this.appName =\n options.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n\n this.prefixLogs = `[${this.appName}] [HealthCheck]`\n\n /** @type {CachedHealthResult | null} */\n this._cachedResult = null\n\n /** @type {Map<string, Pool>} */\n this._databasePools = new Map()\n\n /** @type {DatabaseConfig | null} */\n this._mainDatabaseConfig = null\n\n /** @type {DatabaseConfig[]} */\n this._clusterConfigs = []\n\n this._initDatabases(options)\n\n if (this.redisClient) {\n this._redisClientType = getRedisClientType(this.redisClient)\n }\n }\n\n /**\n * Initialize database configurations from options.\n * @param {Object} options - Constructor options\n * @private\n */\n _initDatabases(options) {\n const mainUrl = options.databaseUrl || process.env.DATABASE_URL || ''\n const mainName = options.databaseName || `${this.appName}_db`\n\n if (mainUrl) {\n this._mainDatabaseConfig = { name: mainName, url: mainUrl }\n }\n\n const additionalUrls = options.additionalDatabaseUrls || {}\n for (const [name, url] of Object.entries(additionalUrls)) {\n if (url) {\n this._clusterConfigs.push({ name, url })\n }\n }\n }\n\n /**\n * Get or create a database pool for a given config.\n * @param {DatabaseConfig} config - Database configuration\n * @returns {Pool}\n * @private\n */\n _getPool(config) {\n if (!this._databasePools.has(config.name)) {\n this._databasePools.set(\n config.name,\n new Pool({\n connectionString: config.url,\n max: 1,\n idleTimeoutMillis: 30000,\n connectionTimeoutMillis: 5000,\n })\n )\n }\n return this._databasePools.get(config.name)\n }\n\n /**\n * Checks if cached result is still valid based on TTL.\n * @returns {boolean}\n * @private\n */\n _isCacheValid() {\n if (!this._cachedResult) return false\n return Date.now() - this._cachedResult.timestamp < this.cacheTtlMs\n }\n\n /**\n * Tests a single database cluster connectivity.\n * @param {DatabaseConfig} config - Database configuration\n * @returns {Promise<ComponentHealth>}\n * @private\n */\n async _checkSingleDatabase(config) {\n const start = Date.now()\n\n try {\n const pool = this._getPool(config)\n await pool.query('SELECT 1')\n return {\n status: 'healthy',\n latencyMs: Date.now() - start,\n }\n } catch (err) {\n return {\n status: 'unhealthy',\n message: maskSensitiveData(err.message),\n latencyMs: Date.now() - start,\n }\n }\n }\n\n /**\n * Tests all PostgreSQL databases (main + clusters) in parallel.\n * @returns {Promise<Object | null>} Database health with optional clusters\n * @private\n */\n async _checkAllDatabases() {\n if (!this._mainDatabaseConfig && this._clusterConfigs.length === 0) {\n return null\n }\n\n // Check main database\n let mainHealth = null\n if (this._mainDatabaseConfig) {\n mainHealth = await this._checkSingleDatabase(this._mainDatabaseConfig)\n }\n\n // Check clusters in parallel\n let clusters = null\n if (this._clusterConfigs.length > 0) {\n const clusterResults = await Promise.all(\n this._clusterConfigs.map(async config => ({\n name: config.name,\n health: await this._checkSingleDatabase(config),\n }))\n )\n\n clusters = {}\n for (const { name, health } of clusterResults) {\n clusters[name] = health\n }\n }\n\n // Calculate overall status\n const allStatuses = []\n if (mainHealth) allStatuses.push(mainHealth.status)\n if (clusters) {\n allStatuses.push(...Object.values(clusters).map(c => c.status))\n }\n\n let overallStatus = 'healthy'\n if (allStatuses.some(s => s === 'unhealthy')) {\n overallStatus = 'unhealthy'\n } else if (allStatuses.some(s => s === 'degraded')) {\n overallStatus = 'degraded'\n }\n\n // Build result\n const result = { status: overallStatus }\n if (mainHealth) {\n result.latencyMs = mainHealth.latencyMs\n if (mainHealth.message) result.message = mainHealth.message\n }\n if (clusters) {\n result.clusters = clusters\n }\n\n return result\n }\n\n /**\n * Tests Redis connectivity using PING command.\n * @returns {Promise<ComponentHealth>}\n * @private\n */\n async _checkRedis() {\n if (!this.redisClient) {\n return { status: 'healthy', message: 'Not configured' }\n }\n\n const start = Date.now()\n\n try {\n let pong\n\n if (this._redisClientType === REDIS_V3) {\n pong = await new Promise((resolve, reject) => {\n this.redisClient.ping((err, result) => {\n if (err) reject(err)\n else resolve(result)\n })\n })\n } else if (\n this._redisClientType === REDIS_V4 ||\n this._redisClientType === IOREDIS\n ) {\n pong = await this.redisClient.ping()\n } else {\n return { status: 'unhealthy', message: 'Unknown Redis client type' }\n }\n\n if (pong === 'PONG') {\n return {\n status: 'healthy',\n latencyMs: Date.now() - start,\n }\n }\n\n return {\n status: 'unhealthy',\n message: maskSensitiveData(`Unexpected PING response: ${pong}`),\n latencyMs: Date.now() - start,\n }\n } catch (err) {\n return {\n status: 'unhealthy',\n message: maskSensitiveData(err.message),\n latencyMs: Date.now() - start,\n }\n }\n }\n\n /**\n * Performs a full health check on all configured components.\n * Results are cached for the configured TTL to prevent excessive load.\n *\n * @returns {Promise<HealthCheckResult>}\n */\n async performHealthCheck() {\n if (this._isCacheValid()) {\n return { ...this._cachedResult.result, cached: true }\n }\n\n const [dbHealth, redisHealth] = await Promise.all([\n this._checkAllDatabases(),\n this._checkRedis(),\n ])\n\n const components = {}\n if (dbHealth) components.database = dbHealth\n if (this.redisClient) components.redis = redisHealth\n\n const statuses = Object.values(components).map(c => c.status)\n let overallStatus = 'healthy'\n\n if (statuses.some(s => s === 'unhealthy')) {\n overallStatus = 'unhealthy'\n } else if (statuses.some(s => s === 'degraded')) {\n overallStatus = 'degraded'\n }\n\n /** @type {HealthCheckResult} */\n const result = {\n status: overallStatus,\n timestamp: new Date().toISOString(),\n cached: false,\n components,\n }\n\n this._cachedResult = {\n result,\n timestamp: Date.now(),\n }\n\n return result\n }\n\n /**\n * Clears the cached health check result, forcing the next check to be fresh.\n */\n clearCache() {\n this._cachedResult = null\n }\n\n /**\n * Builds a list of error messages from health check result.\n * All error messages are sanitized to remove sensitive information.\n * @param {HealthCheckResult} result - Health check result\n * @returns {string[]} Array of sanitized error messages\n * @private\n */\n _getErrorMessages(result) {\n const errors = []\n\n if (result.components.database) {\n const db = result.components.database\n\n // Check main database status\n if (db.status === 'unhealthy' && db.message) {\n const dbName = this._mainDatabaseConfig?.name || 'main'\n errors.push(`DB ${dbName}: ${maskSensitiveData(db.message)}`)\n }\n\n // Check clusters\n if (db.clusters) {\n for (const [name, health] of Object.entries(db.clusters)) {\n if (health.status === 'unhealthy') {\n const message = health.message || 'connection failed'\n errors.push(`DB ${name}: ${maskSensitiveData(message)}`)\n }\n }\n }\n }\n\n if (result.components.redis && result.components.redis.status === 'unhealthy') {\n const message = result.components.redis.message || 'connection failed'\n errors.push(`Redis: ${maskSensitiveData(message)}`)\n }\n\n return errors\n }\n\n /**\n * Express middleware handler for health check endpoint.\n * Returns 200 for healthy/degraded, 503 for unhealthy.\n * Response includes errors array when status is not healthy.\n * All sensitive data (passwords, connection strings, etc.) is masked.\n *\n * @returns {(req: any, res: any) => Promise<void>} Express request handler\n */\n healthHandler() {\n return async (req, res) => {\n try {\n const result = await this.performHealthCheck()\n const statusCode = result.status === 'unhealthy' ? 503 : 200\n \n // Build response with errors if not healthy\n const response = { ...result }\n if (result.status !== 'healthy') {\n const errors = this._getErrorMessages(result)\n if (errors.length > 0) {\n response.errors = errors\n }\n }\n\n res.status(statusCode).json(response)\n } catch (err) {\n console.error(`${this.prefixLogs} Health check failed:`, err)\n res.status(503).json({\n status: 'unhealthy',\n timestamp: new Date().toISOString(),\n cached: false,\n errors: [maskSensitiveData(err.message)],\n })\n }\n }\n }\n\n /**\n * Register health check endpoint on an Express app.\n *\n * @param {import('express').Application} app - Express application\n * @param {string} [path='/health-status'] - Path for the health endpoint\n */\n registerHealthEndpoint(app, path = '/health-status') {\n app.get(path, this.healthHandler())\n console.info(`${this.prefixLogs} Registered health endpoint at ${path}`)\n }\n\n /**\n * Cleanup resources (database pools).\n * @returns {Promise<void>}\n */\n async cleanup() {\n for (const [name, pool] of this._databasePools) {\n try {\n await pool.end()\n } catch (err) {\n console.error(`${this.prefixLogs} Error closing database pool ${name}:`, err)\n }\n }\n this._databasePools.clear()\n }\n}\n\nmodule.exports = { HealthCheckClient }\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
package/src/healthCheckClient.js
CHANGED
|
@@ -8,6 +8,59 @@ const {
|
|
|
8
8
|
|
|
9
9
|
const HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Patterns to detect and mask sensitive information in error messages
|
|
13
|
+
*/
|
|
14
|
+
const SENSITIVE_PATTERNS = [
|
|
15
|
+
// Database connection strings: postgres://user:password@host:port/database
|
|
16
|
+
{
|
|
17
|
+
pattern: /(postgres(?:ql)?|mysql|mongodb|redis|amqp):\/\/([^:]+):([^@]+)@([^:/]+)(:\d+)?\/([^\s?]+)/gi,
|
|
18
|
+
replacement: '$1://***:***@***$5/***',
|
|
19
|
+
},
|
|
20
|
+
// Generic URLs with credentials: protocol://user:password@host
|
|
21
|
+
{
|
|
22
|
+
pattern: /(\w+):\/\/([^:]+):([^@]+)@([^\s/]+)/gi,
|
|
23
|
+
replacement: '$1://***:***@***',
|
|
24
|
+
},
|
|
25
|
+
// Password fields in JSON or key=value format
|
|
26
|
+
{
|
|
27
|
+
pattern: /(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
28
|
+
replacement: '$1=***',
|
|
29
|
+
},
|
|
30
|
+
// Database/table/schema/role/user names in error messages: database "name", table "name", etc.
|
|
31
|
+
{
|
|
32
|
+
pattern: /(database|table|schema|role|user|relation|column|index)\s*["']([^"']+)["']/gi,
|
|
33
|
+
replacement: '$1 "***"',
|
|
34
|
+
},
|
|
35
|
+
// IP addresses (to hide internal network structure)
|
|
36
|
+
{
|
|
37
|
+
pattern: /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?\b/g,
|
|
38
|
+
replacement: '***$2',
|
|
39
|
+
},
|
|
40
|
+
// Host names that might reveal internal infrastructure
|
|
41
|
+
{
|
|
42
|
+
pattern: /\b(host|hostname|server)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
43
|
+
replacement: '$1=***',
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Masks sensitive information in a string
|
|
49
|
+
* @param {string} text - Text that might contain sensitive data
|
|
50
|
+
* @returns {string} - Text with sensitive data masked
|
|
51
|
+
*/
|
|
52
|
+
function maskSensitiveData(text) {
|
|
53
|
+
if (!text || typeof text !== 'string') {
|
|
54
|
+
return text
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let masked = text
|
|
58
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
59
|
+
masked = masked.replace(pattern, replacement)
|
|
60
|
+
}
|
|
61
|
+
return masked
|
|
62
|
+
}
|
|
63
|
+
|
|
11
64
|
/**
|
|
12
65
|
* @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus
|
|
13
66
|
*/
|
|
@@ -81,8 +134,11 @@ class HealthCheckClient {
|
|
|
81
134
|
/** @type {Map<string, Pool>} */
|
|
82
135
|
this._databasePools = new Map()
|
|
83
136
|
|
|
137
|
+
/** @type {DatabaseConfig | null} */
|
|
138
|
+
this._mainDatabaseConfig = null
|
|
139
|
+
|
|
84
140
|
/** @type {DatabaseConfig[]} */
|
|
85
|
-
this.
|
|
141
|
+
this._clusterConfigs = []
|
|
86
142
|
|
|
87
143
|
this._initDatabases(options)
|
|
88
144
|
|
|
@@ -98,16 +154,16 @@ class HealthCheckClient {
|
|
|
98
154
|
*/
|
|
99
155
|
_initDatabases(options) {
|
|
100
156
|
const mainUrl = options.databaseUrl || process.env.DATABASE_URL || ''
|
|
101
|
-
const mainName = options.databaseName ||
|
|
157
|
+
const mainName = options.databaseName || `${this.appName}_db`
|
|
102
158
|
|
|
103
159
|
if (mainUrl) {
|
|
104
|
-
this.
|
|
160
|
+
this._mainDatabaseConfig = { name: mainName, url: mainUrl }
|
|
105
161
|
}
|
|
106
162
|
|
|
107
163
|
const additionalUrls = options.additionalDatabaseUrls || {}
|
|
108
164
|
for (const [name, url] of Object.entries(additionalUrls)) {
|
|
109
165
|
if (url) {
|
|
110
|
-
this.
|
|
166
|
+
this._clusterConfigs.push({ name, url })
|
|
111
167
|
}
|
|
112
168
|
}
|
|
113
169
|
}
|
|
@@ -162,43 +218,69 @@ class HealthCheckClient {
|
|
|
162
218
|
} catch (err) {
|
|
163
219
|
return {
|
|
164
220
|
status: 'unhealthy',
|
|
165
|
-
message: err.message,
|
|
221
|
+
message: maskSensitiveData(err.message),
|
|
166
222
|
latencyMs: Date.now() - start,
|
|
167
223
|
}
|
|
168
224
|
}
|
|
169
225
|
}
|
|
170
226
|
|
|
171
227
|
/**
|
|
172
|
-
* Tests all PostgreSQL
|
|
173
|
-
* @returns {Promise<
|
|
228
|
+
* Tests all PostgreSQL databases (main + clusters) in parallel.
|
|
229
|
+
* @returns {Promise<Object | null>} Database health with optional clusters
|
|
174
230
|
* @private
|
|
175
231
|
*/
|
|
176
232
|
async _checkAllDatabases() {
|
|
177
|
-
if (this.
|
|
233
|
+
if (!this._mainDatabaseConfig && this._clusterConfigs.length === 0) {
|
|
178
234
|
return null
|
|
179
235
|
}
|
|
180
236
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
237
|
+
// Check main database
|
|
238
|
+
let mainHealth = null
|
|
239
|
+
if (this._mainDatabaseConfig) {
|
|
240
|
+
mainHealth = await this._checkSingleDatabase(this._mainDatabaseConfig)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check clusters in parallel
|
|
244
|
+
let clusters = null
|
|
245
|
+
if (this._clusterConfigs.length > 0) {
|
|
246
|
+
const clusterResults = await Promise.all(
|
|
247
|
+
this._clusterConfigs.map(async config => ({
|
|
248
|
+
name: config.name,
|
|
249
|
+
health: await this._checkSingleDatabase(config),
|
|
250
|
+
}))
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
clusters = {}
|
|
254
|
+
for (const { name, health } of clusterResults) {
|
|
255
|
+
clusters[name] = health
|
|
256
|
+
}
|
|
257
|
+
}
|
|
187
258
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
259
|
+
// Calculate overall status
|
|
260
|
+
const allStatuses = []
|
|
261
|
+
if (mainHealth) allStatuses.push(mainHealth.status)
|
|
262
|
+
if (clusters) {
|
|
263
|
+
allStatuses.push(...Object.values(clusters).map(c => c.status))
|
|
191
264
|
}
|
|
192
265
|
|
|
193
|
-
const statuses = Object.values(clusters).map(c => c.status)
|
|
194
266
|
let overallStatus = 'healthy'
|
|
195
|
-
if (
|
|
267
|
+
if (allStatuses.some(s => s === 'unhealthy')) {
|
|
196
268
|
overallStatus = 'unhealthy'
|
|
197
|
-
} else if (
|
|
269
|
+
} else if (allStatuses.some(s => s === 'degraded')) {
|
|
198
270
|
overallStatus = 'degraded'
|
|
199
271
|
}
|
|
200
272
|
|
|
201
|
-
|
|
273
|
+
// Build result
|
|
274
|
+
const result = { status: overallStatus }
|
|
275
|
+
if (mainHealth) {
|
|
276
|
+
result.latencyMs = mainHealth.latencyMs
|
|
277
|
+
if (mainHealth.message) result.message = mainHealth.message
|
|
278
|
+
}
|
|
279
|
+
if (clusters) {
|
|
280
|
+
result.clusters = clusters
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result
|
|
202
284
|
}
|
|
203
285
|
|
|
204
286
|
/**
|
|
@@ -241,13 +323,13 @@ class HealthCheckClient {
|
|
|
241
323
|
|
|
242
324
|
return {
|
|
243
325
|
status: 'unhealthy',
|
|
244
|
-
message: `Unexpected PING response: ${pong}
|
|
326
|
+
message: maskSensitiveData(`Unexpected PING response: ${pong}`),
|
|
245
327
|
latencyMs: Date.now() - start,
|
|
246
328
|
}
|
|
247
329
|
} catch (err) {
|
|
248
330
|
return {
|
|
249
331
|
status: 'unhealthy',
|
|
250
|
-
message: err.message,
|
|
332
|
+
message: maskSensitiveData(err.message),
|
|
251
333
|
latencyMs: Date.now() - start,
|
|
252
334
|
}
|
|
253
335
|
}
|
|
@@ -307,8 +389,9 @@ class HealthCheckClient {
|
|
|
307
389
|
|
|
308
390
|
/**
|
|
309
391
|
* Builds a list of error messages from health check result.
|
|
392
|
+
* All error messages are sanitized to remove sensitive information.
|
|
310
393
|
* @param {HealthCheckResult} result - Health check result
|
|
311
|
-
* @returns {string[]} Array of error messages
|
|
394
|
+
* @returns {string[]} Array of sanitized error messages
|
|
312
395
|
* @private
|
|
313
396
|
*/
|
|
314
397
|
_getErrorMessages(result) {
|
|
@@ -316,17 +399,27 @@ class HealthCheckClient {
|
|
|
316
399
|
|
|
317
400
|
if (result.components.database) {
|
|
318
401
|
const db = result.components.database
|
|
402
|
+
|
|
403
|
+
// Check main database status
|
|
404
|
+
if (db.status === 'unhealthy' && db.message) {
|
|
405
|
+
const dbName = this._mainDatabaseConfig?.name || 'main'
|
|
406
|
+
errors.push(`DB ${dbName}: ${maskSensitiveData(db.message)}`)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check clusters
|
|
319
410
|
if (db.clusters) {
|
|
320
411
|
for (const [name, health] of Object.entries(db.clusters)) {
|
|
321
412
|
if (health.status === 'unhealthy') {
|
|
322
|
-
|
|
413
|
+
const message = health.message || 'connection failed'
|
|
414
|
+
errors.push(`DB ${name}: ${maskSensitiveData(message)}`)
|
|
323
415
|
}
|
|
324
416
|
}
|
|
325
417
|
}
|
|
326
418
|
}
|
|
327
419
|
|
|
328
420
|
if (result.components.redis && result.components.redis.status === 'unhealthy') {
|
|
329
|
-
|
|
421
|
+
const message = result.components.redis.message || 'connection failed'
|
|
422
|
+
errors.push(`Redis: ${maskSensitiveData(message)}`)
|
|
330
423
|
}
|
|
331
424
|
|
|
332
425
|
return errors
|
|
@@ -334,8 +427,9 @@ class HealthCheckClient {
|
|
|
334
427
|
|
|
335
428
|
/**
|
|
336
429
|
* Express middleware handler for health check endpoint.
|
|
337
|
-
* Returns 200
|
|
338
|
-
*
|
|
430
|
+
* Returns 200 for healthy/degraded, 503 for unhealthy.
|
|
431
|
+
* Response includes errors array when status is not healthy.
|
|
432
|
+
* All sensitive data (passwords, connection strings, etc.) is masked.
|
|
339
433
|
*
|
|
340
434
|
* @returns {(req: any, res: any) => Promise<void>} Express request handler
|
|
341
435
|
*/
|
|
@@ -343,21 +437,26 @@ class HealthCheckClient {
|
|
|
343
437
|
return async (req, res) => {
|
|
344
438
|
try {
|
|
345
439
|
const result = await this.performHealthCheck()
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
440
|
+
const statusCode = result.status === 'unhealthy' ? 503 : 200
|
|
441
|
+
|
|
442
|
+
// Build response with errors if not healthy
|
|
443
|
+
const response = { ...result }
|
|
444
|
+
if (result.status !== 'healthy') {
|
|
445
|
+
const errors = this._getErrorMessages(result)
|
|
446
|
+
if (errors.length > 0) {
|
|
447
|
+
response.errors = errors
|
|
448
|
+
}
|
|
350
449
|
}
|
|
351
450
|
|
|
352
|
-
|
|
353
|
-
const errorMessage = errors.length > 0
|
|
354
|
-
? `UNHEALTHY: ${errors.join('; ')}`
|
|
355
|
-
: 'UNHEALTHY: Unknown error'
|
|
356
|
-
|
|
357
|
-
res.status(503).set('Content-Type', 'text/plain').send(errorMessage)
|
|
451
|
+
res.status(statusCode).json(response)
|
|
358
452
|
} catch (err) {
|
|
359
453
|
console.error(`${this.prefixLogs} Health check failed:`, err)
|
|
360
|
-
res.status(503).
|
|
454
|
+
res.status(503).json({
|
|
455
|
+
status: 'unhealthy',
|
|
456
|
+
timestamp: new Date().toISOString(),
|
|
457
|
+
cached: false,
|
|
458
|
+
errors: [maskSensitiveData(err.message)],
|
|
459
|
+
})
|
|
361
460
|
}
|
|
362
461
|
}
|
|
363
462
|
}
|
|
@@ -366,9 +465,9 @@ class HealthCheckClient {
|
|
|
366
465
|
* Register health check endpoint on an Express app.
|
|
367
466
|
*
|
|
368
467
|
* @param {import('express').Application} app - Express application
|
|
369
|
-
* @param {string} [path='/health'] - Path for the health endpoint
|
|
468
|
+
* @param {string} [path='/health-status'] - Path for the health endpoint
|
|
370
469
|
*/
|
|
371
|
-
registerHealthEndpoint(app, path = '/health') {
|
|
470
|
+
registerHealthEndpoint(app, path = '/health-status') {
|
|
372
471
|
app.get(path, this.healthHandler())
|
|
373
472
|
console.info(`${this.prefixLogs} Registered health endpoint at ${path}`)
|
|
374
473
|
}
|