@adalo/metrics 0.1.123 → 0.1.125

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.
Files changed (63) hide show
  1. package/lib/health/healthCheckCache.d.ts +57 -0
  2. package/lib/health/healthCheckCache.d.ts.map +1 -0
  3. package/lib/health/healthCheckCache.js +200 -0
  4. package/lib/health/healthCheckCache.js.map +1 -0
  5. package/lib/{healthCheckClient.d.ts → health/healthCheckClient.d.ts} +54 -12
  6. package/lib/health/healthCheckClient.d.ts.map +1 -0
  7. package/lib/{healthCheckClient.js → health/healthCheckClient.js} +173 -27
  8. package/lib/health/healthCheckClient.js.map +1 -0
  9. package/lib/health/healthCheckUtils.d.ts +54 -0
  10. package/lib/health/healthCheckUtils.d.ts.map +1 -0
  11. package/lib/health/healthCheckUtils.js +142 -0
  12. package/lib/health/healthCheckUtils.js.map +1 -0
  13. package/lib/health/healthCheckWorker.d.ts +2 -0
  14. package/lib/health/healthCheckWorker.d.ts.map +1 -0
  15. package/lib/health/healthCheckWorker.js +64 -0
  16. package/lib/health/healthCheckWorker.js.map +1 -0
  17. package/lib/index.d.ts +8 -6
  18. package/lib/index.d.ts.map +1 -1
  19. package/lib/index.js +28 -6
  20. package/lib/index.js.map +1 -1
  21. package/lib/metrics/baseMetricsClient.d.ts.map +1 -0
  22. package/lib/metrics/baseMetricsClient.js.map +1 -0
  23. package/lib/metrics/metricsClient.d.ts.map +1 -0
  24. package/lib/metrics/metricsClient.js.map +1 -0
  25. package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -0
  26. package/lib/metrics/metricsDatabaseClient.js.map +1 -0
  27. package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -0
  28. package/lib/{metricsQueueRedisClient.js → metrics/metricsQueueRedisClient.js} +2 -2
  29. package/lib/metrics/metricsQueueRedisClient.js.map +1 -0
  30. package/lib/metrics/metricsRedisClient.d.ts.map +1 -0
  31. package/lib/{metricsRedisClient.js → metrics/metricsRedisClient.js} +1 -1
  32. package/lib/metrics/metricsRedisClient.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/health/healthCheckCache.js +237 -0
  35. package/src/{healthCheckClient.js → health/healthCheckClient.js} +171 -31
  36. package/src/health/healthCheckUtils.js +143 -0
  37. package/src/health/healthCheckWorker.js +61 -0
  38. package/src/index.ts +8 -6
  39. package/src/{metricsQueueRedisClient.js → metrics/metricsQueueRedisClient.js} +2 -2
  40. package/src/{metricsRedisClient.js → metrics/metricsRedisClient.js} +1 -1
  41. package/lib/baseMetricsClient.d.ts.map +0 -1
  42. package/lib/baseMetricsClient.js.map +0 -1
  43. package/lib/healthCheckClient.d.ts.map +0 -1
  44. package/lib/healthCheckClient.js.map +0 -1
  45. package/lib/metricsClient.d.ts.map +0 -1
  46. package/lib/metricsClient.js.map +0 -1
  47. package/lib/metricsDatabaseClient.d.ts.map +0 -1
  48. package/lib/metricsDatabaseClient.js.map +0 -1
  49. package/lib/metricsQueueRedisClient.d.ts.map +0 -1
  50. package/lib/metricsQueueRedisClient.js.map +0 -1
  51. package/lib/metricsRedisClient.d.ts.map +0 -1
  52. package/lib/metricsRedisClient.js.map +0 -1
  53. /package/lib/{baseMetricsClient.d.ts → metrics/baseMetricsClient.d.ts} +0 -0
  54. /package/lib/{baseMetricsClient.js → metrics/baseMetricsClient.js} +0 -0
  55. /package/lib/{metricsClient.d.ts → metrics/metricsClient.d.ts} +0 -0
  56. /package/lib/{metricsClient.js → metrics/metricsClient.js} +0 -0
  57. /package/lib/{metricsDatabaseClient.d.ts → metrics/metricsDatabaseClient.d.ts} +0 -0
  58. /package/lib/{metricsDatabaseClient.js → metrics/metricsDatabaseClient.js} +0 -0
  59. /package/lib/{metricsQueueRedisClient.d.ts → metrics/metricsQueueRedisClient.d.ts} +0 -0
  60. /package/lib/{metricsRedisClient.d.ts → metrics/metricsRedisClient.d.ts} +0 -0
  61. /package/src/{baseMetricsClient.js → metrics/baseMetricsClient.js} +0 -0
  62. /package/src/{metricsClient.js → metrics/metricsClient.js} +0 -0
  63. /package/src/{metricsDatabaseClient.js → metrics/metricsDatabaseClient.js} +0 -0
@@ -8,7 +8,10 @@ const {
8
8
  REDIS_V4,
9
9
  IOREDIS,
10
10
  REDIS_V3
11
- } = require('./redisUtils');
11
+ } = require('../redisUtils');
12
+ const {
13
+ HealthCheckCache
14
+ } = require('./healthCheckCache');
12
15
  const HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000;
13
16
 
14
17
  /**
@@ -72,7 +75,8 @@ function maskSensitiveData(text) {
72
75
  /**
73
76
  * @typedef {Object} ComponentHealth
74
77
  * @property {HealthStatus} status - Component health status
75
- * @property {string} [message] - Optional status message
78
+ * @property {string} [error] - Error message if status is unhealthy
79
+ * @property {string} [message] - Optional status message (deprecated, use error)
76
80
  * @property {number} [latencyMs] - Connection latency in milliseconds
77
81
  */
78
82
 
@@ -86,8 +90,8 @@ function maskSensitiveData(text) {
86
90
  * @typedef {Object} HealthCheckResult
87
91
  * @property {HealthStatus} status - Overall health status
88
92
  * @property {string} timestamp - ISO timestamp of the check
89
- * @property {boolean} cached - Whether this result is from cache
90
- * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health
93
+ * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} checks - Individual service health checks
94
+ * @property {string[]} [errors] - Top-level error messages (not related to specific services)
91
95
  */
92
96
 
93
97
  /**
@@ -120,12 +124,14 @@ class HealthCheckClient {
120
124
  * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL
121
125
  * @param {string} [options.databaseName='main'] - Name for the main database
122
126
  * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)
123
- * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)
127
+ * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis) - used for cache
128
+ * @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in results (for backend)
124
129
  * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)
125
130
  * @param {string} [options.appName] - Application name for logging
126
131
  */
127
132
  constructor(options = {}) {
128
133
  this.redisClient = options.redisClient || null;
134
+ this.includeRedisCheck = options.includeRedisCheck || false;
129
135
  this.cacheTtlMs = options.cacheTtlMs ?? HEALTH_CHECK_CACHE_TTL_MS;
130
136
  this.appName = options.appName || process.env.BUILD_APP_NAME || 'unknown-app';
131
137
  this.prefixLogs = `[${this.appName}] [HealthCheck]`;
@@ -133,6 +139,9 @@ class HealthCheckClient {
133
139
  /** @type {CachedHealthResult | null} */
134
140
  this._cachedResult = null;
135
141
 
142
+ /** @type {Promise<HealthCheckResult> | null} */
143
+ this._refreshPromise = null;
144
+
136
145
  /** @type {Map<string, Pool>} */
137
146
  this._databasePools = new Map();
138
147
 
@@ -145,6 +154,11 @@ class HealthCheckClient {
145
154
  if (this.redisClient) {
146
155
  this._redisClientType = getRedisClientType(this.redisClient);
147
156
  }
157
+ this._cache = new HealthCheckCache({
158
+ redisClient: this.redisClient,
159
+ appName: this.appName,
160
+ cacheTtlMs: this.cacheTtlMs
161
+ });
148
162
  }
149
163
 
150
164
  /**
@@ -218,7 +232,7 @@ class HealthCheckClient {
218
232
  } catch (err) {
219
233
  return {
220
234
  status: 'unhealthy',
221
- message: maskSensitiveData(err.message),
235
+ error: maskSensitiveData(err.message),
222
236
  latencyMs: Date.now() - start
223
237
  };
224
238
  }
@@ -275,6 +289,8 @@ class HealthCheckClient {
275
289
  };
276
290
  if (mainHealth) {
277
291
  result.latencyMs = mainHealth.latencyMs;
292
+ if (mainHealth.error) result.error = mainHealth.error;
293
+ // Keep message for backward compatibility
278
294
  if (mainHealth.message) result.message = mainHealth.message;
279
295
  }
280
296
  if (clusters) {
@@ -335,21 +351,76 @@ class HealthCheckClient {
335
351
  /**
336
352
  * Performs a full health check on all configured components.
337
353
  * Results are cached for the configured TTL to prevent excessive load.
354
+ * Uses a mutex pattern to prevent concurrent health checks when cache expires.
355
+ * If cache is expired but a refresh is in progress, returns stale cache (if available).
338
356
  *
339
357
  * @returns {Promise<HealthCheckResult>}
340
358
  */
341
359
  async performHealthCheck() {
360
+ // If cache is valid, return immediately
342
361
  if (this._isCacheValid()) {
343
362
  return {
344
363
  ...this._cachedResult.result,
345
364
  cached: true
346
365
  };
347
366
  }
348
- const [dbHealth, redisHealth] = await Promise.all([this._checkAllDatabases(), this._checkRedis()]);
349
- const components = {};
350
- if (dbHealth) components.database = dbHealth;
351
- if (this.redisClient) components.redis = redisHealth;
352
- const statuses = Object.values(components).map(c => c.status);
367
+
368
+ // If a refresh is already in progress, return stale cache (if available) or wait for refresh
369
+ if (this._refreshPromise) {
370
+ // Return stale cache immediately if available, otherwise wait for refresh
371
+ if (this._cachedResult) {
372
+ return {
373
+ ...this._cachedResult.result,
374
+ cached: true
375
+ };
376
+ }
377
+ // Wait for the in-progress refresh
378
+ return this._refreshPromise;
379
+ }
380
+
381
+ // Start a new refresh
382
+ this._refreshPromise = this._performHealthCheckInternal().then(result => {
383
+ this._refreshPromise = null;
384
+ return result;
385
+ }).catch(err => {
386
+ this._refreshPromise = null;
387
+ throw err;
388
+ });
389
+
390
+ // Return stale cache if available while refresh is happening, otherwise wait
391
+ if (this._cachedResult) {
392
+ // Return stale cache immediately, refresh will happen in background
393
+ return {
394
+ ...this._cachedResult.result,
395
+ cached: true
396
+ };
397
+ }
398
+
399
+ // No stale cache available, wait for refresh
400
+ return this._refreshPromise;
401
+ }
402
+
403
+ /**
404
+ * Internal method that actually performs the health check.
405
+ * This is separated to allow the mutex pattern in performHealthCheck.
406
+ * Only checks database - Redis is used only for cache, not health check.
407
+ *
408
+ * @returns {Promise<HealthCheckResult>}
409
+ * @private
410
+ */
411
+ async _performHealthCheckInternal() {
412
+ // Check database
413
+ const dbHealth = await this._checkAllDatabases();
414
+
415
+ // Optionally check Redis (for backend)
416
+ let redisHealth = null;
417
+ if (this.includeRedisCheck && this.redisClient) {
418
+ redisHealth = await this._checkRedis();
419
+ }
420
+ const checks = {};
421
+ if (dbHealth) checks.database = dbHealth;
422
+ if (redisHealth) checks.redis = redisHealth;
423
+ const statuses = Object.values(checks).map(c => c.status);
353
424
  let overallStatus = 'healthy';
354
425
  if (statuses.some(s => s === 'unhealthy')) {
355
426
  overallStatus = 'unhealthy';
@@ -361,8 +432,7 @@ class HealthCheckClient {
361
432
  const result = {
362
433
  status: overallStatus,
363
434
  timestamp: new Date().toISOString(),
364
- cached: false,
365
- components
435
+ checks
366
436
  };
367
437
  this._cachedResult = {
368
438
  result,
@@ -371,11 +441,58 @@ class HealthCheckClient {
371
441
  return result;
372
442
  }
373
443
 
444
+ /**
445
+ * Gets cached result from shared cache (Redis if available, otherwise in-memory).
446
+ * Returns null if no cache is available. This is used by endpoints to read-only access.
447
+ * If Redis fails, returns error result with proper format.
448
+ *
449
+ * @returns {Promise<HealthCheckResult | null>} Cached result, error result if Redis fails, or null if not available
450
+ */
451
+ async getCachedResult() {
452
+ try {
453
+ const cached = await this._cache.get();
454
+ if (cached) {
455
+ return cached;
456
+ }
457
+ // No cache available - worker may not have run yet
458
+ return null;
459
+ } catch (err) {
460
+ // Redis read failed - return error result with proper format
461
+ console.error(`${this.prefixLogs} Failed to read from cache:`, err);
462
+ const errorMessage = maskSensitiveData(err.message || 'Cache read failed');
463
+ return {
464
+ status: 'unhealthy',
465
+ timestamp: new Date().toISOString(),
466
+ checks: {
467
+ redis: {
468
+ status: 'unhealthy',
469
+ error: errorMessage
470
+ }
471
+ },
472
+ errors: [errorMessage]
473
+ };
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Forces a refresh of the health check cache.
479
+ * This is used by background workers to periodically update the cache.
480
+ * Updates shared cache (Redis if available, otherwise in-memory).
481
+ *
482
+ * @returns {Promise<HealthCheckResult>}
483
+ */
484
+ async refreshCache() {
485
+ const result = await this._performHealthCheckInternal();
486
+ await this._cache.set(result);
487
+ return result;
488
+ }
489
+
374
490
  /**
375
491
  * Clears the cached health check result, forcing the next check to be fresh.
376
492
  */
377
493
  clearCache() {
378
494
  this._cachedResult = null;
495
+ this._refreshPromise = null;
379
496
  }
380
497
 
381
498
  /**
@@ -387,27 +504,28 @@ class HealthCheckClient {
387
504
  */
388
505
  _getErrorMessages(result) {
389
506
  const errors = [];
390
- if (result.components.database) {
391
- const db = result.components.database;
507
+ if (result.checks.database) {
508
+ const db = result.checks.database;
392
509
 
393
510
  // Check main database status
394
- if (db.status === 'unhealthy' && db.message) {
511
+ if (db.status === 'unhealthy' && (db.message || db.error)) {
395
512
  const dbName = this._mainDatabaseConfig?.name || 'main';
396
- errors.push(`DB ${dbName}: ${maskSensitiveData(db.message)}`);
513
+ const errorMsg = db.error || db.message;
514
+ errors.push(`DB ${dbName}: ${maskSensitiveData(errorMsg)}`);
397
515
  }
398
516
 
399
517
  // Check clusters
400
518
  if (db.clusters) {
401
519
  for (const [name, health] of Object.entries(db.clusters)) {
402
520
  if (health.status === 'unhealthy') {
403
- const message = health.message || 'connection failed';
521
+ const message = health.error || health.message || 'connection failed';
404
522
  errors.push(`DB ${name}: ${maskSensitiveData(message)}`);
405
523
  }
406
524
  }
407
525
  }
408
526
  }
409
- if (result.components.redis && result.components.redis.status === 'unhealthy') {
410
- const message = result.components.redis.message || 'connection failed';
527
+ if (result.checks.redis && result.checks.redis.status === 'unhealthy') {
528
+ const message = result.checks.redis.error || result.checks.redis.message || 'connection failed';
411
529
  errors.push(`Redis: ${maskSensitiveData(message)}`);
412
530
  }
413
531
  return errors;
@@ -419,19 +537,41 @@ class HealthCheckClient {
419
537
  * Response includes errors array when status is not healthy.
420
538
  * All sensitive data (passwords, connection strings, etc.) is masked.
421
539
  *
540
+ * This handler only reads from cache and never triggers database queries.
541
+ * Use a background worker to periodically refresh the cache.
542
+ *
422
543
  * @returns {(req: any, res: any) => Promise<void>} Express request handler
423
544
  */
424
545
  healthHandler() {
425
546
  return async (req, res) => {
426
547
  try {
427
- const result = await this.performHealthCheck();
548
+ // Only read from cache, never trigger DB queries
549
+ const result = await this.getCachedResult();
550
+ if (!result) {
551
+ // No cache available - return unhealthy status with proper format
552
+ const errorMsg = 'Health check cache not available. Worker may not be running.';
553
+ res.status(503).json({
554
+ status: 'unhealthy',
555
+ timestamp: new Date().toISOString(),
556
+ checks: {
557
+ redis: {
558
+ status: 'unhealthy',
559
+ error: errorMsg
560
+ }
561
+ },
562
+ errors: [errorMsg]
563
+ });
564
+ return;
565
+ }
428
566
  const statusCode = result.status === 'unhealthy' ? 503 : 200;
429
567
 
430
- // Build response with errors if not healthy
568
+ // Build response - errors are already in result if present
431
569
  const response = {
432
570
  ...result
433
571
  };
434
- if (result.status !== 'healthy') {
572
+
573
+ // Add top-level errors array if not healthy and errors not already present
574
+ if (result.status !== 'healthy' && !result.errors) {
435
575
  const errors = this._getErrorMessages(result);
436
576
  if (errors.length > 0) {
437
577
  response.errors = errors;
@@ -440,11 +580,17 @@ class HealthCheckClient {
440
580
  res.status(statusCode).json(response);
441
581
  } catch (err) {
442
582
  console.error(`${this.prefixLogs} Health check failed:`, err);
583
+ const errorMsg = maskSensitiveData(err.message);
443
584
  res.status(503).json({
444
585
  status: 'unhealthy',
445
586
  timestamp: new Date().toISOString(),
446
- cached: false,
447
- errors: [maskSensitiveData(err.message)]
587
+ checks: {
588
+ redis: {
589
+ status: 'unhealthy',
590
+ error: errorMsg
591
+ }
592
+ },
593
+ errors: [errorMsg]
448
594
  });
449
595
  }
450
596
  };
@@ -454,9 +600,9 @@ class HealthCheckClient {
454
600
  * Register health check endpoint on an Express app.
455
601
  *
456
602
  * @param {import('express').Application} app - Express application
457
- * @param {string} [path='/health-status'] - Path for the health endpoint
603
+ * @param {string} [path='/health'] - Path for the health endpoint
458
604
  */
459
- registerHealthEndpoint(app, path = '/health-status') {
605
+ registerHealthEndpoint(app, path = '/health') {
460
606
  app.get(path, this.healthHandler());
461
607
  console.info(`${this.prefixLogs} Registered health endpoint at ${path}`);
462
608
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthCheckClient.js","names":["Pool","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","HealthCheckCache","HEALTH_CHECK_CACHE_TTL_MS","SENSITIVE_PATTERNS","pattern","replacement","maskSensitiveData","text","masked","replace","HealthCheckClient","constructor","options","redisClient","includeRedisCheck","cacheTtlMs","appName","process","env","BUILD_APP_NAME","prefixLogs","_cachedResult","_refreshPromise","_databasePools","Map","_mainDatabaseConfig","_clusterConfigs","_initDatabases","_redisClientType","_cache","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","error","message","_checkAllDatabases","length","mainHealth","clusters","clusterResults","Promise","all","map","health","allStatuses","values","c","overallStatus","some","s","result","_checkRedis","pong","resolve","reject","ping","performHealthCheck","cached","_performHealthCheckInternal","then","catch","dbHealth","redisHealth","checks","database","redis","statuses","toISOString","getCachedResult","console","errorMessage","errors","refreshCache","clearCache","_getErrorMessages","db","dbName","errorMsg","healthHandler","req","res","json","statusCode","response","registerHealthEndpoint","app","path","info","cleanup","end","clear","module","exports"],"sources":["../../src/health/healthCheckClient.js"],"sourcesContent":["const { Pool } = require('pg')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('../redisUtils')\nconst { HealthCheckCache } = require('./healthCheckCache')\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} [error] - Error message if status is unhealthy\n * @property {string} [message] - Optional status message (deprecated, use error)\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 {Object<string, ComponentHealth | DatabaseClusterHealth>} checks - Individual service health checks\n * @property {string[]} [errors] - Top-level error messages (not related to specific services)\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) - used for cache\n * @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in results (for backend)\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.includeRedisCheck = options.includeRedisCheck || false\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 {Promise<HealthCheckResult> | null} */\n this._refreshPromise = 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 this._cache = new HealthCheckCache({\n redisClient: this.redisClient,\n appName: this.appName,\n cacheTtlMs: this.cacheTtlMs,\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 error: 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.error) result.error = mainHealth.error\n // Keep message for backward compatibility\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 * Uses a mutex pattern to prevent concurrent health checks when cache expires.\n * If cache is expired but a refresh is in progress, returns stale cache (if available).\n *\n * @returns {Promise<HealthCheckResult>}\n */\n async performHealthCheck() {\n // If cache is valid, return immediately\n if (this._isCacheValid()) {\n return { ...this._cachedResult.result, cached: true }\n }\n\n // If a refresh is already in progress, return stale cache (if available) or wait for refresh\n if (this._refreshPromise) {\n // Return stale cache immediately if available, otherwise wait for refresh\n if (this._cachedResult) {\n return { ...this._cachedResult.result, cached: true }\n }\n // Wait for the in-progress refresh\n return this._refreshPromise\n }\n\n // Start a new refresh\n this._refreshPromise = this._performHealthCheckInternal()\n .then(result => {\n this._refreshPromise = null\n return result\n })\n .catch(err => {\n this._refreshPromise = null\n throw err\n })\n\n // Return stale cache if available while refresh is happening, otherwise wait\n if (this._cachedResult) {\n // Return stale cache immediately, refresh will happen in background\n return { ...this._cachedResult.result, cached: true }\n }\n\n // No stale cache available, wait for refresh\n return this._refreshPromise\n }\n\n /**\n * Internal method that actually performs the health check.\n * This is separated to allow the mutex pattern in performHealthCheck.\n * Only checks database - Redis is used only for cache, not health check.\n *\n * @returns {Promise<HealthCheckResult>}\n * @private\n */\n async _performHealthCheckInternal() {\n // Check database\n const dbHealth = await this._checkAllDatabases()\n\n // Optionally check Redis (for backend)\n let redisHealth = null\n if (this.includeRedisCheck && this.redisClient) {\n redisHealth = await this._checkRedis()\n }\n\n const checks = {}\n if (dbHealth) checks.database = dbHealth\n if (redisHealth) checks.redis = redisHealth\n\n const statuses = Object.values(checks).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 checks,\n }\n\n this._cachedResult = {\n result,\n timestamp: Date.now(),\n }\n\n return result\n }\n\n /**\n * Gets cached result from shared cache (Redis if available, otherwise in-memory).\n * Returns null if no cache is available. This is used by endpoints to read-only access.\n * If Redis fails, returns error result with proper format.\n *\n * @returns {Promise<HealthCheckResult | null>} Cached result, error result if Redis fails, or null if not available\n */\n async getCachedResult() {\n try {\n const cached = await this._cache.get()\n if (cached) {\n return cached\n }\n // No cache available - worker may not have run yet\n return null\n } catch (err) {\n // Redis read failed - return error result with proper format\n console.error(`${this.prefixLogs} Failed to read from cache:`, err)\n const errorMessage = maskSensitiveData(err.message || 'Cache read failed')\n return {\n status: 'unhealthy',\n timestamp: new Date().toISOString(),\n checks: {\n redis: {\n status: 'unhealthy',\n error: errorMessage,\n },\n },\n errors: [errorMessage],\n }\n }\n }\n\n /**\n * Forces a refresh of the health check cache.\n * This is used by background workers to periodically update the cache.\n * Updates shared cache (Redis if available, otherwise in-memory).\n *\n * @returns {Promise<HealthCheckResult>}\n */\n async refreshCache() {\n const result = await this._performHealthCheckInternal()\n await this._cache.set(result)\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 this._refreshPromise = 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.checks.database) {\n const db = result.checks.database\n\n // Check main database status\n if (db.status === 'unhealthy' && (db.message || db.error)) {\n const dbName = this._mainDatabaseConfig?.name || 'main'\n const errorMsg = db.error || db.message\n errors.push(`DB ${dbName}: ${maskSensitiveData(errorMsg)}`)\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.error || health.message || 'connection failed'\n errors.push(`DB ${name}: ${maskSensitiveData(message)}`)\n }\n }\n }\n }\n\n if (result.checks.redis && result.checks.redis.status === 'unhealthy') {\n const message = result.checks.redis.error || result.checks.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 * This handler only reads from cache and never triggers database queries.\n * Use a background worker to periodically refresh the cache.\n *\n * @returns {(req: any, res: any) => Promise<void>} Express request handler\n */\n healthHandler() {\n return async (req, res) => {\n try {\n // Only read from cache, never trigger DB queries\n const result = await this.getCachedResult()\n\n if (!result) {\n // No cache available - return unhealthy status with proper format\n const errorMsg = 'Health check cache not available. Worker may not be running.'\n res.status(503).json({\n status: 'unhealthy',\n timestamp: new Date().toISOString(),\n checks: {\n redis: {\n status: 'unhealthy',\n error: errorMsg,\n },\n },\n errors: [errorMsg],\n })\n return\n }\n\n const statusCode = result.status === 'unhealthy' ? 503 : 200\n\n // Build response - errors are already in result if present\n const response = { ...result }\n \n // Add top-level errors array if not healthy and errors not already present\n if (result.status !== 'healthy' && !result.errors) {\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 const errorMsg = maskSensitiveData(err.message)\n res.status(503).json({\n status: 'unhealthy',\n timestamp: new Date().toISOString(),\n checks: {\n redis: {\n status: 'unhealthy',\n error: errorMsg,\n },\n },\n errors: [errorMsg],\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":[]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Creates a Redis client for health check cache with a specific client name.
3
+ * This allows the cache to be shared across worker and web processes.
4
+ *
5
+ * @param {Object} options
6
+ * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)
7
+ * @param {string} [options.appName] - Application name for client name
8
+ * @param {string} [options.clientName='healthcheck'] - Client name suffix
9
+ * @returns {any | null} Redis client instance or null if REDIS_URL not available
10
+ */
11
+ export function createHealthCheckRedisClient(options?: {
12
+ redisUrl?: string | undefined;
13
+ appName?: string | undefined;
14
+ clientName?: string | undefined;
15
+ }): any | null;
16
+ /**
17
+ * Creates a health check client for worker process.
18
+ * Worker needs database config to perform health checks and store results in Redis.
19
+ *
20
+ * @param {Object} options
21
+ * @param {string} options.databaseUrl - Main database URL
22
+ * @param {string} options.databaseName - Database name
23
+ * @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
24
+ * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
25
+ * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)
26
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
27
+ * @returns {HealthCheckClient} Configured health check client instance for worker
28
+ */
29
+ export function createHealthCheckWorkerClient(options: {
30
+ databaseUrl: string;
31
+ databaseName: string;
32
+ appName?: string | undefined;
33
+ additionalDatabaseUrls?: {
34
+ [x: string]: string;
35
+ } | undefined;
36
+ redisUrl?: string | undefined;
37
+ cacheTtlMs?: number | undefined;
38
+ }): import("./healthCheckClient").HealthCheckClient;
39
+ /**
40
+ * Creates a health check client for endpoint handlers.
41
+ * Endpoints only read from cache, so they only need Redis client (no database config).
42
+ *
43
+ * @param {Object} options
44
+ * @param {any} options.redisClient - Existing Redis client instance (from system) - REQUIRED
45
+ * @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response (for backend)
46
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
47
+ * @returns {HealthCheckClient} Configured health check client instance for endpoints
48
+ */
49
+ export function createHealthCheckEndpointClient(options: {
50
+ redisClient: any;
51
+ includeRedisCheck?: boolean | undefined;
52
+ cacheTtlMs?: number | undefined;
53
+ }): import("./healthCheckClient").HealthCheckClient;
54
+ //# sourceMappingURL=healthCheckUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthCheckUtils.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckUtils.js"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH;IAL4B,QAAQ;IACR,OAAO;IACP,UAAU;IACzB,GAAG,GAAG,IAAI,CAmDtB;AAED;;;;;;;;;;;;GAYG;AACH;IAR2B,WAAW,EAA3B,MAAM;IACU,YAAY,EAA5B,MAAM;IACW,OAAO;IACS,sBAAsB;;;IACtC,QAAQ;IACR,UAAU;oDAmCrC;AAED;;;;;;;;;GASG;AACH;IALwB,WAAW,EAAxB,GAAG;IACe,iBAAiB;IAClB,UAAU;oDAiBrC"}
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+
3
+ const redis = require('redis');
4
+ const {
5
+ getRedisClientType,
6
+ REDIS_V3,
7
+ REDIS_V4
8
+ } = require('../redisUtils');
9
+
10
+ /**
11
+ * Creates a Redis client for health check cache with a specific client name.
12
+ * This allows the cache to be shared across worker and web processes.
13
+ *
14
+ * @param {Object} options
15
+ * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)
16
+ * @param {string} [options.appName] - Application name for client name
17
+ * @param {string} [options.clientName='healthcheck'] - Client name suffix
18
+ * @returns {any | null} Redis client instance or null if REDIS_URL not available
19
+ */
20
+ function createHealthCheckRedisClient(options = {}) {
21
+ const redisUrl = options.redisUrl || process.env.REDIS_URL;
22
+ const appName = options.appName || process.env.METRICS_APP_NAME || process.env.BUILD_APP_NAME || 'unknown-app';
23
+ const clientName = options.clientName || 'healthcheck';
24
+ if (!redisUrl) {
25
+ console.warn(`[HealthCheck] REDIS_URL not configured for ${appName}, cache will be in-memory only (not shared across processes)`);
26
+ return null;
27
+ }
28
+ try {
29
+ const client = redis.createClient({
30
+ url: redisUrl,
31
+ retry_strategy: retryOptions => {
32
+ if (retryOptions.attempt > 3) {
33
+ console.warn(`[HealthCheck] Redis client connection failed after max retries for ${appName}`);
34
+ return undefined;
35
+ }
36
+ return 2000;
37
+ }
38
+ });
39
+
40
+ // Set client name for identification
41
+ if (process.env.NODE_ENV !== 'test') {
42
+ client.on('connect', () => {
43
+ const fullClientName = `${appName}:${clientName}`;
44
+ client.send_command('CLIENT', ['SETNAME', fullClientName], err => {
45
+ if (err) {
46
+ console.error(`[HealthCheck] Failed to set client name for ${fullClientName}:`, err);
47
+ } else {
48
+ console.log(`[HealthCheck] Connected to Redis for pid:${process.pid} (${fullClientName})`);
49
+ }
50
+ });
51
+ });
52
+ }
53
+ client.on('error', err => {
54
+ console.warn(`[HealthCheck] Redis client error for ${appName}:`, err.message);
55
+ });
56
+ return client;
57
+ } catch (err) {
58
+ console.warn(`[HealthCheck] Failed to create Redis client for ${appName}:`, err.message);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Creates a health check client for worker process.
65
+ * Worker needs database config to perform health checks and store results in Redis.
66
+ *
67
+ * @param {Object} options
68
+ * @param {string} options.databaseUrl - Main database URL
69
+ * @param {string} options.databaseName - Database name
70
+ * @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
71
+ * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
72
+ * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)
73
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
74
+ * @returns {HealthCheckClient} Configured health check client instance for worker
75
+ */
76
+ function createHealthCheckWorkerClient(options) {
77
+ const {
78
+ HealthCheckClient
79
+ } = require('./healthCheckClient');
80
+ const {
81
+ databaseUrl,
82
+ databaseName,
83
+ appName,
84
+ additionalDatabaseUrls,
85
+ redisUrl,
86
+ includeRedisCheck = false,
87
+ cacheTtlMs = 60000
88
+ } = options;
89
+ const redisClient = createHealthCheckRedisClient({
90
+ redisUrl,
91
+ appName,
92
+ clientName: 'healthcheck-worker'
93
+ });
94
+ const clientOptions = {
95
+ databaseUrl,
96
+ databaseName,
97
+ appName: appName || process.env.BUILD_APP_NAME || 'unknown-app',
98
+ redisClient,
99
+ includeRedisCheck,
100
+ cacheTtlMs
101
+ };
102
+ if (additionalDatabaseUrls) {
103
+ clientOptions.additionalDatabaseUrls = additionalDatabaseUrls;
104
+ }
105
+ return new HealthCheckClient(clientOptions);
106
+ }
107
+
108
+ /**
109
+ * Creates a health check client for endpoint handlers.
110
+ * Endpoints only read from cache, so they only need Redis client (no database config).
111
+ *
112
+ * @param {Object} options
113
+ * @param {any} options.redisClient - Existing Redis client instance (from system) - REQUIRED
114
+ * @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response (for backend)
115
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
116
+ * @returns {HealthCheckClient} Configured health check client instance for endpoints
117
+ */
118
+ function createHealthCheckEndpointClient(options) {
119
+ const {
120
+ HealthCheckClient
121
+ } = require('./healthCheckClient');
122
+ const {
123
+ redisClient,
124
+ includeRedisCheck = false,
125
+ cacheTtlMs = 60000
126
+ } = options;
127
+ if (!redisClient) {
128
+ throw new Error('redisClient is required for createHealthCheckEndpointClient');
129
+ }
130
+ return new HealthCheckClient({
131
+ appName: process.env.BUILD_APP_NAME || 'unknown-app',
132
+ redisClient,
133
+ cacheTtlMs,
134
+ includeRedisCheck
135
+ });
136
+ }
137
+ module.exports = {
138
+ createHealthCheckRedisClient,
139
+ createHealthCheckWorkerClient,
140
+ createHealthCheckEndpointClient
141
+ };
142
+ //# sourceMappingURL=healthCheckUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthCheckUtils.js","names":["redis","require","getRedisClientType","REDIS_V3","REDIS_V4","createHealthCheckRedisClient","options","redisUrl","process","env","REDIS_URL","appName","METRICS_APP_NAME","BUILD_APP_NAME","clientName","console","warn","client","createClient","url","retry_strategy","retryOptions","attempt","undefined","NODE_ENV","on","fullClientName","send_command","err","error","log","pid","message","createHealthCheckWorkerClient","HealthCheckClient","databaseUrl","databaseName","additionalDatabaseUrls","includeRedisCheck","cacheTtlMs","redisClient","clientOptions","createHealthCheckEndpointClient","Error","module","exports"],"sources":["../../src/health/healthCheckUtils.js"],"sourcesContent":["const redis = require('redis')\nconst { getRedisClientType, REDIS_V3, REDIS_V4 } = require('../redisUtils')\n\n/**\n * Creates a Redis client for health check cache with a specific client name.\n * This allows the cache to be shared across worker and web processes.\n *\n * @param {Object} options\n * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)\n * @param {string} [options.appName] - Application name for client name\n * @param {string} [options.clientName='healthcheck'] - Client name suffix\n * @returns {any | null} Redis client instance or null if REDIS_URL not available\n */\nfunction createHealthCheckRedisClient(options = {}) {\n const redisUrl = options.redisUrl || process.env.REDIS_URL\n const appName = options.appName || process.env.METRICS_APP_NAME || process.env.BUILD_APP_NAME || 'unknown-app'\n const clientName = options.clientName || 'healthcheck'\n\n if (!redisUrl) {\n console.warn(`[HealthCheck] REDIS_URL not configured for ${appName}, cache will be in-memory only (not shared across processes)`)\n return null\n }\n\n try {\n const client = redis.createClient({\n url: redisUrl,\n retry_strategy: retryOptions => {\n if (retryOptions.attempt > 3) {\n console.warn(`[HealthCheck] Redis client connection failed after max retries for ${appName}`)\n return undefined\n }\n return 2000\n },\n })\n\n // Set client name for identification\n if (process.env.NODE_ENV !== 'test') {\n client.on('connect', () => {\n const fullClientName = `${appName}:${clientName}`\n client.send_command(\n 'CLIENT',\n ['SETNAME', fullClientName],\n err => {\n if (err) {\n console.error(`[HealthCheck] Failed to set client name for ${fullClientName}:`, err)\n } else {\n console.log(`[HealthCheck] Connected to Redis for pid:${process.pid} (${fullClientName})`)\n }\n }\n )\n })\n }\n\n client.on('error', err => {\n console.warn(`[HealthCheck] Redis client error for ${appName}:`, err.message)\n })\n\n return client\n } catch (err) {\n console.warn(`[HealthCheck] Failed to create Redis client for ${appName}:`, err.message)\n return null\n }\n}\n\n/**\n * Creates a health check client for worker process.\n * Worker needs database config to perform health checks and store results in Redis.\n *\n * @param {Object} options\n * @param {string} options.databaseUrl - Main database URL\n * @param {string} options.databaseName - Database name\n * @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)\n * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)\n * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds\n * @returns {HealthCheckClient} Configured health check client instance for worker\n */\nfunction createHealthCheckWorkerClient(options) {\n const { HealthCheckClient } = require('./healthCheckClient')\n const {\n databaseUrl,\n databaseName,\n appName,\n additionalDatabaseUrls,\n redisUrl,\n includeRedisCheck = false,\n cacheTtlMs = 60000,\n } = options\n\n const redisClient = createHealthCheckRedisClient({\n redisUrl,\n appName,\n clientName: 'healthcheck-worker',\n })\n\n const clientOptions = {\n databaseUrl,\n databaseName,\n appName: appName || process.env.BUILD_APP_NAME || 'unknown-app',\n redisClient,\n includeRedisCheck,\n cacheTtlMs,\n }\n\n if (additionalDatabaseUrls) {\n clientOptions.additionalDatabaseUrls = additionalDatabaseUrls\n }\n\n return new HealthCheckClient(clientOptions)\n}\n\n/**\n * Creates a health check client for endpoint handlers.\n * Endpoints only read from cache, so they only need Redis client (no database config).\n *\n * @param {Object} options\n * @param {any} options.redisClient - Existing Redis client instance (from system) - REQUIRED\n * @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response (for backend)\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds\n * @returns {HealthCheckClient} Configured health check client instance for endpoints\n */\nfunction createHealthCheckEndpointClient(options) {\n const { HealthCheckClient } = require('./healthCheckClient')\n const { redisClient, includeRedisCheck = false, cacheTtlMs = 60000 } = options\n\n if (!redisClient) {\n throw new Error('redisClient is required for createHealthCheckEndpointClient')\n }\n\n return new HealthCheckClient({\n appName: process.env.BUILD_APP_NAME || 'unknown-app',\n redisClient,\n cacheTtlMs,\n includeRedisCheck,\n })\n}\n\nmodule.exports = {\n createHealthCheckRedisClient,\n createHealthCheckWorkerClient,\n createHealthCheckEndpointClient,\n}\n\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ export function createHealthCheckWorker(options: any): () => Promise<void>;
2
+ //# sourceMappingURL=healthCheckWorker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthCheckWorker.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckWorker.js"],"names":[],"mappings":"AAiBA,2EA2CC"}