@adalo/metrics 0.1.120 → 0.1.121
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.
|
@@ -188,9 +188,17 @@ export class HealthCheckClient {
|
|
|
188
188
|
* Clears the cached health check result, forcing the next check to be fresh.
|
|
189
189
|
*/
|
|
190
190
|
clearCache(): void;
|
|
191
|
+
/**
|
|
192
|
+
* Builds a list of error messages from health check result.
|
|
193
|
+
* @param {HealthCheckResult} result - Health check result
|
|
194
|
+
* @returns {string[]} Array of error messages
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
private _getErrorMessages;
|
|
191
198
|
/**
|
|
192
199
|
* Express middleware handler for health check endpoint.
|
|
193
|
-
* Returns 200 for healthy
|
|
200
|
+
* Returns 200 with "OK" for healthy, 503 with error details for unhealthy.
|
|
201
|
+
* Uses plain text response for BetterStack compatibility.
|
|
194
202
|
*
|
|
195
203
|
* @returns {(req: any, res: any) => Promise<void>} Express request handler
|
|
196
204
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../src/healthCheckClient.js"],"names":[],"mappings":"2BAWa,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;OAwBlC;IArBC,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,+BAA+B;IAC/B,kBADW,cAAc,EAAE,CACD;IAKxB,qCAA4D;IAIhE;;;;OAIG;IACH,uBAcC;IAED;;;;;OAKG;IACH,iBAaC;IAED;;;;OAIG;IACH,sBAGC;IAED;;;;;OAKG;IACH,6BAiBC;IAED;;;;OAIG;IACH,2BA0BC;IAED;;;;OAIG;IACH,oBA6CC;IAED;;;;;OAKG;IACH,sBAFa,QAAQ,iBAAiB,CAAC,CAuCtC;IAED;;OAEG;IACH,mBAEC;IAED;;;;;OAKG;IACH,uBAFmB,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../src/healthCheckClient.js"],"names":[],"mappings":"2BAWa,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;OAwBlC;IArBC,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,+BAA+B;IAC/B,kBADW,cAAc,EAAE,CACD;IAKxB,qCAA4D;IAIhE;;;;OAIG;IACH,uBAcC;IAED;;;;;OAKG;IACH,iBAaC;IAED;;;;OAIG;IACH,sBAGC;IAED;;;;;OAKG;IACH,6BAiBC;IAED;;;;OAIG;IACH,2BA0BC;IAED;;;;OAIG;IACH,oBA6CC;IAED;;;;;OAKG;IACH,sBAFa,QAAQ,iBAAiB,CAAC,CAuCtC;IAED;;OAEG;IACH,mBAEC;IAED;;;;;OAKG;IACH,0BAmBC;IAED;;;;;;OAMG;IACH,uBAFmB,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,CAuBjD;IAED;;;;;OAKG;IACH,kEAGC;IAED;;;OAGG;IACH,WAFa,QAAQ,IAAI,CAAC,CAWzB;CACF"}
|
package/lib/healthCheckClient.js
CHANGED
|
@@ -295,9 +295,34 @@ class HealthCheckClient {
|
|
|
295
295
|
this._cachedResult = null;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Builds a list of error messages from health check result.
|
|
300
|
+
* @param {HealthCheckResult} result - Health check result
|
|
301
|
+
* @returns {string[]} Array of error messages
|
|
302
|
+
* @private
|
|
303
|
+
*/
|
|
304
|
+
_getErrorMessages(result) {
|
|
305
|
+
const errors = [];
|
|
306
|
+
if (result.components.database) {
|
|
307
|
+
const db = result.components.database;
|
|
308
|
+
if (db.clusters) {
|
|
309
|
+
for (const [name, health] of Object.entries(db.clusters)) {
|
|
310
|
+
if (health.status === 'unhealthy') {
|
|
311
|
+
errors.push(`DB ${name}: ${health.message || 'connection failed'}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (result.components.redis && result.components.redis.status === 'unhealthy') {
|
|
317
|
+
errors.push(`Redis: ${result.components.redis.message || 'connection failed'}`);
|
|
318
|
+
}
|
|
319
|
+
return errors;
|
|
320
|
+
}
|
|
321
|
+
|
|
298
322
|
/**
|
|
299
323
|
* Express middleware handler for health check endpoint.
|
|
300
|
-
* Returns 200 for healthy
|
|
324
|
+
* Returns 200 with "OK" for healthy, 503 with error details for unhealthy.
|
|
325
|
+
* Uses plain text response for BetterStack compatibility.
|
|
301
326
|
*
|
|
302
327
|
* @returns {(req: any, res: any) => Promise<void>} Express request handler
|
|
303
328
|
*/
|
|
@@ -305,16 +330,16 @@ class HealthCheckClient {
|
|
|
305
330
|
return async (req, res) => {
|
|
306
331
|
try {
|
|
307
332
|
const result = await this.performHealthCheck();
|
|
308
|
-
|
|
309
|
-
|
|
333
|
+
if (result.status === 'healthy') {
|
|
334
|
+
res.status(200).set('Content-Type', 'text/plain').send('OK');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const errors = this._getErrorMessages(result);
|
|
338
|
+
const errorMessage = errors.length > 0 ? `UNHEALTHY: ${errors.join('; ')}` : 'UNHEALTHY: Unknown error';
|
|
339
|
+
res.status(503).set('Content-Type', 'text/plain').send(errorMessage);
|
|
310
340
|
} catch (err) {
|
|
311
341
|
console.error(`${this.prefixLogs} Health check failed:`, err);
|
|
312
|
-
res.status(503).
|
|
313
|
-
status: 'unhealthy',
|
|
314
|
-
timestamp: new Date().toISOString(),
|
|
315
|
-
cached: false,
|
|
316
|
-
error: err.message
|
|
317
|
-
});
|
|
342
|
+
res.status(503).set('Content-Type', 'text/plain').send(`UNHEALTHY: ${err.message}`);
|
|
318
343
|
}
|
|
319
344
|
};
|
|
320
345
|
}
|
|
@@ -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","healthHandler","req","res","statusCode","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 * @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 * Express middleware handler for health check endpoint.\n * Returns 200 for healthy/degraded, 503 for unhealthy.\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 res.status(statusCode).json(result)\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 error: 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":";;AAAA,MAAM;EAAEA;AAAK,CAAC,GAAGC,OAAO,CAAC,IAAI,CAAC;AAC9B,MAAM;EACJC,kBAAkB;EAClBC,QAAQ;EACRC,OAAO;EACPC;AACF,CAAC,GAAGJ,OAAO,CAAC,cAAc,CAAC;AAE3B,MAAMK,yBAAyB,GAAG,EAAE,GAAG,IAAI;;AAE3C;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,iBAAiB,CAAC;EACtB;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAACC,OAAO,GAAG,CAAC,CAAC,EAAE;IACxB,IAAI,CAACC,WAAW,GAAGD,OAAO,CAACC,WAAW,IAAI,IAAI;IAC9C,IAAI,CAACC,UAAU,GAAGF,OAAO,CAACE,UAAU,IAAIL,yBAAyB;IACjE,IAAI,CAACM,OAAO,GACVH,OAAO,CAACG,OAAO,IAAIC,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;IAEhE,IAAI,CAACC,UAAU,GAAG,IAAI,IAAI,CAACJ,OAAO,iBAAiB;;IAEnD;IACA,IAAI,CAACK,aAAa,GAAG,IAAI;;IAEzB;IACA,IAAI,CAACC,cAAc,GAAG,IAAIC,GAAG,CAAC,CAAC;;IAE/B;IACA,IAAI,CAACC,gBAAgB,GAAG,EAAE;IAE1B,IAAI,CAACC,cAAc,CAACZ,OAAO,CAAC;IAE5B,IAAI,IAAI,CAACC,WAAW,EAAE;MACpB,IAAI,CAACY,gBAAgB,GAAGpB,kBAAkB,CAAC,IAAI,CAACQ,WAAW,CAAC;IAC9D;EACF;;EAEA;AACF;AACA;AACA;AACA;EACEW,cAAcA,CAACZ,OAAO,EAAE;IACtB,MAAMc,OAAO,GAAGd,OAAO,CAACe,WAAW,IAAIX,OAAO,CAACC,GAAG,CAACW,YAAY,IAAI,EAAE;IACrE,MAAMC,QAAQ,GAAGjB,OAAO,CAACkB,YAAY,IAAI,MAAM;IAE/C,IAAIJ,OAAO,EAAE;MACX,IAAI,CAACH,gBAAgB,CAACQ,IAAI,CAAC;QAAEC,IAAI,EAAEH,QAAQ;QAAEI,GAAG,EAAEP;MAAQ,CAAC,CAAC;IAC9D;IAEA,MAAMQ,cAAc,GAAGtB,OAAO,CAACuB,sBAAsB,IAAI,CAAC,CAAC;IAC3D,KAAK,MAAM,CAACH,IAAI,EAAEC,GAAG,CAAC,IAAIG,MAAM,CAACC,OAAO,CAACH,cAAc,CAAC,EAAE;MACxD,IAAID,GAAG,EAAE;QACP,IAAI,CAACV,gBAAgB,CAACQ,IAAI,CAAC;UAAEC,IAAI;UAAEC;QAAI,CAAC,CAAC;MAC3C;IACF;EACF;;EAEA;AACF;AACA;AACA;AACA;AACA;EACEK,QAAQA,CAACC,MAAM,EAAE;IACf,IAAI,CAAC,IAAI,CAAClB,cAAc,CAACmB,GAAG,CAACD,MAAM,CAACP,IAAI,CAAC,EAAE;MACzC,IAAI,CAACX,cAAc,CAACoB,GAAG,CACrBF,MAAM,CAACP,IAAI,EACX,IAAI7B,IAAI,CAAC;QACPuC,gBAAgB,EAAEH,MAAM,CAACN,GAAG;QAC5BU,GAAG,EAAE,CAAC;QACNC,iBAAiB,EAAE,KAAK;QACxBC,uBAAuB,EAAE;MAC3B,CAAC,CACH,CAAC;IACH;IACA,OAAO,IAAI,CAACxB,cAAc,CAACyB,GAAG,CAACP,MAAM,CAACP,IAAI,CAAC;EAC7C;;EAEA;AACF;AACA;AACA;AACA;EACEe,aAAaA,CAAA,EAAG;IACd,IAAI,CAAC,IAAI,CAAC3B,aAAa,EAAE,OAAO,KAAK;IACrC,OAAO4B,IAAI,CAACC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC7B,aAAa,CAAC8B,SAAS,GAAG,IAAI,CAACpC,UAAU;EACpE;;EAEA;AACF;AACA;AACA;AACA;AACA;EACE,MAAMqC,oBAAoBA,CAACZ,MAAM,EAAE;IACjC,MAAMa,KAAK,GAAGJ,IAAI,CAACC,GAAG,CAAC,CAAC;IAExB,IAAI;MACF,MAAMI,IAAI,GAAG,IAAI,CAACf,QAAQ,CAACC,MAAM,CAAC;MAClC,MAAMc,IAAI,CAACC,KAAK,CAAC,UAAU,CAAC;MAC5B,OAAO;QACLC,MAAM,EAAE,SAAS;QACjBC,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH,CAAC,CAAC,OAAOK,GAAG,EAAE;MACZ,OAAO;QACLF,MAAM,EAAE,WAAW;QACnBG,OAAO,EAAED,GAAG,CAACC,OAAO;QACpBF,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH;EACF;;EAEA;AACF;AACA;AACA;AACA;EACE,MAAMO,kBAAkBA,CAAA,EAAG;IACzB,IAAI,IAAI,CAACpC,gBAAgB,CAACqC,MAAM,KAAK,CAAC,EAAE;MACtC,OAAO,IAAI;IACb;IAEA,MAAMC,OAAO,GAAG,MAAMC,OAAO,CAACC,GAAG,CAC/B,IAAI,CAACxC,gBAAgB,CAACyC,GAAG,CAAC,MAAMzB,MAAM,KAAK;MACzCP,IAAI,EAAEO,MAAM,CAACP,IAAI;MACjBiC,MAAM,EAAE,MAAM,IAAI,CAACd,oBAAoB,CAACZ,MAAM;IAChD,CAAC,CAAC,CACJ,CAAC;IAED,MAAM2B,QAAQ,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM;MAAElC,IAAI;MAAEiC;IAAO,CAAC,IAAIJ,OAAO,EAAE;MACtCK,QAAQ,CAAClC,IAAI,CAAC,GAAGiC,MAAM;IACzB;IAEA,MAAME,QAAQ,GAAG/B,MAAM,CAACgC,MAAM,CAACF,QAAQ,CAAC,CAACF,GAAG,CAACK,CAAC,IAAIA,CAAC,CAACd,MAAM,CAAC;IAC3D,IAAIe,aAAa,GAAG,SAAS;IAC7B,IAAIH,QAAQ,CAACI,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,WAAW,CAAC,EAAE;MACzCF,aAAa,GAAG,WAAW;IAC7B,CAAC,MAAM,IAAIH,QAAQ,CAACI,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,UAAU,CAAC,EAAE;MAC/CF,aAAa,GAAG,UAAU;IAC5B;IAEA,OAAO;MAAEf,MAAM,EAAEe,aAAa;MAAEJ;IAAS,CAAC;EAC5C;;EAEA;AACF;AACA;AACA;AACA;EACE,MAAMO,WAAWA,CAAA,EAAG;IAClB,IAAI,CAAC,IAAI,CAAC5D,WAAW,EAAE;MACrB,OAAO;QAAE0C,MAAM,EAAE,SAAS;QAAEG,OAAO,EAAE;MAAiB,CAAC;IACzD;IAEA,MAAMN,KAAK,GAAGJ,IAAI,CAACC,GAAG,CAAC,CAAC;IAExB,IAAI;MACF,IAAIyB,IAAI;MAER,IAAI,IAAI,CAACjD,gBAAgB,KAAKjB,QAAQ,EAAE;QACtCkE,IAAI,GAAG,MAAM,IAAIZ,OAAO,CAAC,CAACa,OAAO,EAAEC,MAAM,KAAK;UAC5C,IAAI,CAAC/D,WAAW,CAACgE,IAAI,CAAC,CAACpB,GAAG,EAAEqB,MAAM,KAAK;YACrC,IAAIrB,GAAG,EAAEmB,MAAM,CAACnB,GAAG,CAAC,MACfkB,OAAO,CAACG,MAAM,CAAC;UACtB,CAAC,CAAC;QACJ,CAAC,CAAC;MACJ,CAAC,MAAM,IACL,IAAI,CAACrD,gBAAgB,KAAKnB,QAAQ,IAClC,IAAI,CAACmB,gBAAgB,KAAKlB,OAAO,EACjC;QACAmE,IAAI,GAAG,MAAM,IAAI,CAAC7D,WAAW,CAACgE,IAAI,CAAC,CAAC;MACtC,CAAC,MAAM;QACL,OAAO;UAAEtB,MAAM,EAAE,WAAW;UAAEG,OAAO,EAAE;QAA4B,CAAC;MACtE;MAEA,IAAIgB,IAAI,KAAK,MAAM,EAAE;QACnB,OAAO;UACLnB,MAAM,EAAE,SAAS;UACjBC,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;QAC1B,CAAC;MACH;MAEA,OAAO;QACLG,MAAM,EAAE,WAAW;QACnBG,OAAO,EAAE,6BAA6BgB,IAAI,EAAE;QAC5ClB,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH,CAAC,CAAC,OAAOK,GAAG,EAAE;MACZ,OAAO;QACLF,MAAM,EAAE,WAAW;QACnBG,OAAO,EAAED,GAAG,CAACC,OAAO;QACpBF,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH;EACF;;EAEA;AACF;AACA;AACA;AACA;AACA;EACE,MAAM2B,kBAAkBA,CAAA,EAAG;IACzB,IAAI,IAAI,CAAChC,aAAa,CAAC,CAAC,EAAE;MACxB,OAAO;QAAE,GAAG,IAAI,CAAC3B,aAAa,CAAC0D,MAAM;QAAEE,MAAM,EAAE;MAAK,CAAC;IACvD;IAEA,MAAM,CAACC,QAAQ,EAAEC,WAAW,CAAC,GAAG,MAAMpB,OAAO,CAACC,GAAG,CAAC,CAChD,IAAI,CAACJ,kBAAkB,CAAC,CAAC,EACzB,IAAI,CAACc,WAAW,CAAC,CAAC,CACnB,CAAC;IAEF,MAAMU,UAAU,GAAG,CAAC,CAAC;IACrB,IAAIF,QAAQ,EAAEE,UAAU,CAACC,QAAQ,GAAGH,QAAQ;IAC5C,IAAI,IAAI,CAACpE,WAAW,EAAEsE,UAAU,CAACE,KAAK,GAAGH,WAAW;IAEpD,MAAMf,QAAQ,GAAG/B,MAAM,CAACgC,MAAM,CAACe,UAAU,CAAC,CAACnB,GAAG,CAACK,CAAC,IAAIA,CAAC,CAACd,MAAM,CAAC;IAC7D,IAAIe,aAAa,GAAG,SAAS;IAE7B,IAAIH,QAAQ,CAACI,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,WAAW,CAAC,EAAE;MACzCF,aAAa,GAAG,WAAW;IAC7B,CAAC,MAAM,IAAIH,QAAQ,CAACI,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,UAAU,CAAC,EAAE;MAC/CF,aAAa,GAAG,UAAU;IAC5B;;IAEA;IACA,MAAMQ,MAAM,GAAG;MACbvB,MAAM,EAAEe,aAAa;MACrBpB,SAAS,EAAE,IAAIF,IAAI,CAAC,CAAC,CAACsC,WAAW,CAAC,CAAC;MACnCN,MAAM,EAAE,KAAK;MACbG;IACF,CAAC;IAED,IAAI,CAAC/D,aAAa,GAAG;MACnB0D,MAAM;MACN5B,SAAS,EAAEF,IAAI,CAACC,GAAG,CAAC;IACtB,CAAC;IAED,OAAO6B,MAAM;EACf;;EAEA;AACF;AACA;EACES,UAAUA,CAAA,EAAG;IACX,IAAI,CAACnE,aAAa,GAAG,IAAI;EAC3B;;EAEA;AACF;AACA;AACA;AACA;AACA;EACEoE,aAAaA,CAAA,EAAG;IACd,OAAO,OAAOC,GAAG,EAAEC,GAAG,KAAK;MACzB,IAAI;QACF,MAAMZ,MAAM,GAAG,MAAM,IAAI,CAACC,kBAAkB,CAAC,CAAC;QAC9C,MAAMY,UAAU,GAAGb,MAAM,CAACvB,MAAM,KAAK,WAAW,GAAG,GAAG,GAAG,GAAG;QAE5DmC,GAAG,CAACnC,MAAM,CAACoC,UAAU,CAAC,CAACC,IAAI,CAACd,MAAM,CAAC;MACrC,CAAC,CAAC,OAAOrB,GAAG,EAAE;QACZoC,OAAO,CAACC,KAAK,CAAC,GAAG,IAAI,CAAC3E,UAAU,uBAAuB,EAAEsC,GAAG,CAAC;QAC7DiC,GAAG,CAACnC,MAAM,CAAC,GAAG,CAAC,CAACqC,IAAI,CAAC;UACnBrC,MAAM,EAAE,WAAW;UACnBL,SAAS,EAAE,IAAIF,IAAI,CAAC,CAAC,CAACsC,WAAW,CAAC,CAAC;UACnCN,MAAM,EAAE,KAAK;UACbc,KAAK,EAAErC,GAAG,CAACC;QACb,CAAC,CAAC;MACJ;IACF,CAAC;EACH;;EAEA;AACF;AACA;AACA;AACA;AACA;EACEqC,sBAAsBA,CAACC,GAAG,EAAEC,IAAI,GAAG,SAAS,EAAE;IAC5CD,GAAG,CAAClD,GAAG,CAACmD,IAAI,EAAE,IAAI,CAACT,aAAa,CAAC,CAAC,CAAC;IACnCK,OAAO,CAACK,IAAI,CAAC,GAAG,IAAI,CAAC/E,UAAU,kCAAkC8E,IAAI,EAAE,CAAC;EAC1E;;EAEA;AACF;AACA;AACA;EACE,MAAME,OAAOA,CAAA,EAAG;IACd,KAAK,MAAM,CAACnE,IAAI,EAAEqB,IAAI,CAAC,IAAI,IAAI,CAAChC,cAAc,EAAE;MAC9C,IAAI;QACF,MAAMgC,IAAI,CAAC+C,GAAG,CAAC,CAAC;MAClB,CAAC,CAAC,OAAO3C,GAAG,EAAE;QACZoC,OAAO,CAACC,KAAK,CAAC,GAAG,IAAI,CAAC3E,UAAU,gCAAgCa,IAAI,GAAG,EAAEyB,GAAG,CAAC;MAC/E;IACF;IACA,IAAI,CAACpC,cAAc,CAACgF,KAAK,CAAC,CAAC;EAC7B;AACF;AAEAC,MAAM,CAACC,OAAO,GAAG;EAAE7F;AAAkB,CAAC","ignoreList":[]}
|
|
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":[]}
|
package/package.json
CHANGED
package/src/healthCheckClient.js
CHANGED
|
@@ -305,9 +305,37 @@ class HealthCheckClient {
|
|
|
305
305
|
this._cachedResult = null
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Builds a list of error messages from health check result.
|
|
310
|
+
* @param {HealthCheckResult} result - Health check result
|
|
311
|
+
* @returns {string[]} Array of error messages
|
|
312
|
+
* @private
|
|
313
|
+
*/
|
|
314
|
+
_getErrorMessages(result) {
|
|
315
|
+
const errors = []
|
|
316
|
+
|
|
317
|
+
if (result.components.database) {
|
|
318
|
+
const db = result.components.database
|
|
319
|
+
if (db.clusters) {
|
|
320
|
+
for (const [name, health] of Object.entries(db.clusters)) {
|
|
321
|
+
if (health.status === 'unhealthy') {
|
|
322
|
+
errors.push(`DB ${name}: ${health.message || 'connection failed'}`)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (result.components.redis && result.components.redis.status === 'unhealthy') {
|
|
329
|
+
errors.push(`Redis: ${result.components.redis.message || 'connection failed'}`)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return errors
|
|
333
|
+
}
|
|
334
|
+
|
|
308
335
|
/**
|
|
309
336
|
* Express middleware handler for health check endpoint.
|
|
310
|
-
* Returns 200 for healthy
|
|
337
|
+
* Returns 200 with "OK" for healthy, 503 with error details for unhealthy.
|
|
338
|
+
* Uses plain text response for BetterStack compatibility.
|
|
311
339
|
*
|
|
312
340
|
* @returns {(req: any, res: any) => Promise<void>} Express request handler
|
|
313
341
|
*/
|
|
@@ -315,17 +343,21 @@ class HealthCheckClient {
|
|
|
315
343
|
return async (req, res) => {
|
|
316
344
|
try {
|
|
317
345
|
const result = await this.performHealthCheck()
|
|
318
|
-
const statusCode = result.status === 'unhealthy' ? 503 : 200
|
|
319
346
|
|
|
320
|
-
|
|
347
|
+
if (result.status === 'healthy') {
|
|
348
|
+
res.status(200).set('Content-Type', 'text/plain').send('OK')
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const errors = this._getErrorMessages(result)
|
|
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)
|
|
321
358
|
} catch (err) {
|
|
322
359
|
console.error(`${this.prefixLogs} Health check failed:`, err)
|
|
323
|
-
res.status(503).
|
|
324
|
-
status: 'unhealthy',
|
|
325
|
-
timestamp: new Date().toISOString(),
|
|
326
|
-
cached: false,
|
|
327
|
-
error: err.message,
|
|
328
|
-
})
|
|
360
|
+
res.status(503).set('Content-Type', 'text/plain').send(`UNHEALTHY: ${err.message}`)
|
|
329
361
|
}
|
|
330
362
|
}
|
|
331
363
|
}
|