@adalo/metrics 0.1.122 → 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.
|
@@ -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;
|
|
@@ -209,7 +211,7 @@ export class HealthCheckClient {
|
|
|
209
211
|
* Register health check endpoint on an Express app.
|
|
210
212
|
*
|
|
211
213
|
* @param {import('express').Application} app - Express application
|
|
212
|
-
* @param {string} [path='/health'] - Path for the health endpoint
|
|
214
|
+
* @param {string} [path='/health-status'] - Path for the health endpoint
|
|
213
215
|
*/
|
|
214
216
|
registerHealthEndpoint(app: any, path?: string | undefined): void;
|
|
215
217
|
/**
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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
|
@@ -136,8 +136,11 @@ class HealthCheckClient {
|
|
|
136
136
|
/** @type {Map<string, Pool>} */
|
|
137
137
|
this._databasePools = new Map();
|
|
138
138
|
|
|
139
|
+
/** @type {DatabaseConfig | null} */
|
|
140
|
+
this._mainDatabaseConfig = null;
|
|
141
|
+
|
|
139
142
|
/** @type {DatabaseConfig[]} */
|
|
140
|
-
this.
|
|
143
|
+
this._clusterConfigs = [];
|
|
141
144
|
this._initDatabases(options);
|
|
142
145
|
if (this.redisClient) {
|
|
143
146
|
this._redisClientType = getRedisClientType(this.redisClient);
|
|
@@ -151,17 +154,17 @@ class HealthCheckClient {
|
|
|
151
154
|
*/
|
|
152
155
|
_initDatabases(options) {
|
|
153
156
|
const mainUrl = options.databaseUrl || process.env.DATABASE_URL || '';
|
|
154
|
-
const mainName = options.databaseName ||
|
|
157
|
+
const mainName = options.databaseName || `${this.appName}_db`;
|
|
155
158
|
if (mainUrl) {
|
|
156
|
-
this.
|
|
159
|
+
this._mainDatabaseConfig = {
|
|
157
160
|
name: mainName,
|
|
158
161
|
url: mainUrl
|
|
159
|
-
}
|
|
162
|
+
};
|
|
160
163
|
}
|
|
161
164
|
const additionalUrls = options.additionalDatabaseUrls || {};
|
|
162
165
|
for (const [name, url] of Object.entries(additionalUrls)) {
|
|
163
166
|
if (url) {
|
|
164
|
-
this.
|
|
167
|
+
this._clusterConfigs.push({
|
|
165
168
|
name,
|
|
166
169
|
url
|
|
167
170
|
});
|
|
@@ -222,36 +225,62 @@ class HealthCheckClient {
|
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
/**
|
|
225
|
-
* Tests all PostgreSQL
|
|
226
|
-
* @returns {Promise<
|
|
228
|
+
* Tests all PostgreSQL databases (main + clusters) in parallel.
|
|
229
|
+
* @returns {Promise<Object | null>} Database health with optional clusters
|
|
227
230
|
* @private
|
|
228
231
|
*/
|
|
229
232
|
async _checkAllDatabases() {
|
|
230
|
-
if (this.
|
|
233
|
+
if (!this._mainDatabaseConfig && this._clusterConfigs.length === 0) {
|
|
231
234
|
return null;
|
|
232
235
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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));
|
|
243
264
|
}
|
|
244
|
-
const statuses = Object.values(clusters).map(c => c.status);
|
|
245
265
|
let overallStatus = 'healthy';
|
|
246
|
-
if (
|
|
266
|
+
if (allStatuses.some(s => s === 'unhealthy')) {
|
|
247
267
|
overallStatus = 'unhealthy';
|
|
248
|
-
} else if (
|
|
268
|
+
} else if (allStatuses.some(s => s === 'degraded')) {
|
|
249
269
|
overallStatus = 'degraded';
|
|
250
270
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
271
|
+
|
|
272
|
+
// Build result
|
|
273
|
+
const result = {
|
|
274
|
+
status: overallStatus
|
|
254
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;
|
|
255
284
|
}
|
|
256
285
|
|
|
257
286
|
/**
|
|
@@ -360,6 +389,14 @@ class HealthCheckClient {
|
|
|
360
389
|
const errors = [];
|
|
361
390
|
if (result.components.database) {
|
|
362
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
|
|
363
400
|
if (db.clusters) {
|
|
364
401
|
for (const [name, health] of Object.entries(db.clusters)) {
|
|
365
402
|
if (health.status === 'unhealthy') {
|
|
@@ -417,9 +454,9 @@ class HealthCheckClient {
|
|
|
417
454
|
* Register health check endpoint on an Express app.
|
|
418
455
|
*
|
|
419
456
|
* @param {import('express').Application} app - Express application
|
|
420
|
-
* @param {string} [path='/health'] - Path for the health endpoint
|
|
457
|
+
* @param {string} [path='/health-status'] - Path for the health endpoint
|
|
421
458
|
*/
|
|
422
|
-
registerHealthEndpoint(app, path = '/health') {
|
|
459
|
+
registerHealthEndpoint(app, path = '/health-status') {
|
|
423
460
|
app.get(path, this.healthHandler());
|
|
424
461
|
console.info(`${this.prefixLogs} Registered health endpoint at ${path}`);
|
|
425
462
|
}
|
|
@@ -1 +1 @@
|
|
|
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","_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","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[]} */\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: maskSensitiveData(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: 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 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'] - 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
|
@@ -134,8 +134,11 @@ class HealthCheckClient {
|
|
|
134
134
|
/** @type {Map<string, Pool>} */
|
|
135
135
|
this._databasePools = new Map()
|
|
136
136
|
|
|
137
|
+
/** @type {DatabaseConfig | null} */
|
|
138
|
+
this._mainDatabaseConfig = null
|
|
139
|
+
|
|
137
140
|
/** @type {DatabaseConfig[]} */
|
|
138
|
-
this.
|
|
141
|
+
this._clusterConfigs = []
|
|
139
142
|
|
|
140
143
|
this._initDatabases(options)
|
|
141
144
|
|
|
@@ -151,16 +154,16 @@ class HealthCheckClient {
|
|
|
151
154
|
*/
|
|
152
155
|
_initDatabases(options) {
|
|
153
156
|
const mainUrl = options.databaseUrl || process.env.DATABASE_URL || ''
|
|
154
|
-
const mainName = options.databaseName ||
|
|
157
|
+
const mainName = options.databaseName || `${this.appName}_db`
|
|
155
158
|
|
|
156
159
|
if (mainUrl) {
|
|
157
|
-
this.
|
|
160
|
+
this._mainDatabaseConfig = { name: mainName, url: mainUrl }
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
const additionalUrls = options.additionalDatabaseUrls || {}
|
|
161
164
|
for (const [name, url] of Object.entries(additionalUrls)) {
|
|
162
165
|
if (url) {
|
|
163
|
-
this.
|
|
166
|
+
this._clusterConfigs.push({ name, url })
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
}
|
|
@@ -222,36 +225,62 @@ class HealthCheckClient {
|
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
/**
|
|
225
|
-
* Tests all PostgreSQL
|
|
226
|
-
* @returns {Promise<
|
|
228
|
+
* Tests all PostgreSQL databases (main + clusters) in parallel.
|
|
229
|
+
* @returns {Promise<Object | null>} Database health with optional clusters
|
|
227
230
|
* @private
|
|
228
231
|
*/
|
|
229
232
|
async _checkAllDatabases() {
|
|
230
|
-
if (this.
|
|
233
|
+
if (!this._mainDatabaseConfig && this._clusterConfigs.length === 0) {
|
|
231
234
|
return null
|
|
232
235
|
}
|
|
233
236
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
|
240
258
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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))
|
|
244
264
|
}
|
|
245
265
|
|
|
246
|
-
const statuses = Object.values(clusters).map(c => c.status)
|
|
247
266
|
let overallStatus = 'healthy'
|
|
248
|
-
if (
|
|
267
|
+
if (allStatuses.some(s => s === 'unhealthy')) {
|
|
249
268
|
overallStatus = 'unhealthy'
|
|
250
|
-
} else if (
|
|
269
|
+
} else if (allStatuses.some(s => s === 'degraded')) {
|
|
251
270
|
overallStatus = 'degraded'
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
|
|
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
|
|
255
284
|
}
|
|
256
285
|
|
|
257
286
|
/**
|
|
@@ -370,6 +399,14 @@ class HealthCheckClient {
|
|
|
370
399
|
|
|
371
400
|
if (result.components.database) {
|
|
372
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
|
|
373
410
|
if (db.clusters) {
|
|
374
411
|
for (const [name, health] of Object.entries(db.clusters)) {
|
|
375
412
|
if (health.status === 'unhealthy') {
|
|
@@ -428,9 +465,9 @@ class HealthCheckClient {
|
|
|
428
465
|
* Register health check endpoint on an Express app.
|
|
429
466
|
*
|
|
430
467
|
* @param {import('express').Application} app - Express application
|
|
431
|
-
* @param {string} [path='/health'] - Path for the health endpoint
|
|
468
|
+
* @param {string} [path='/health-status'] - Path for the health endpoint
|
|
432
469
|
*/
|
|
433
|
-
registerHealthEndpoint(app, path = '/health') {
|
|
470
|
+
registerHealthEndpoint(app, path = '/health-status') {
|
|
434
471
|
app.get(path, this.healthHandler())
|
|
435
472
|
console.info(`${this.prefixLogs} Registered health endpoint at ${path}`)
|
|
436
473
|
}
|