@adalo/metrics 0.1.121 → 0.1.123

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