@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/degraded, 503 for unhealthy.
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,CAmBjD;IAED;;;;;OAKG;IACH,kEAGC;IAED;;;OAGG;IACH,WAFa,QAAQ,IAAI,CAAC,CAWzB;CACF"}
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"}
@@ -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/degraded, 503 for unhealthy.
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
- const statusCode = result.status === 'unhealthy' ? 503 : 200;
309
- res.status(statusCode).json(result);
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).json({
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":"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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","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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adalo/metrics",
3
- "version": "0.1.120",
3
+ "version": "0.1.121",
4
4
  "description": "Reusable metrics utilities for Node.js and Laravel apps",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -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/degraded, 503 for unhealthy.
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
- res.status(statusCode).json(result)
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).json({
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
  }