@adalo/metrics 0.1.122 → 0.1.124

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} +59 -15
  6. package/lib/health/healthCheckClient.d.ts.map +1 -0
  7. package/lib/{healthCheckClient.js → health/healthCheckClient.js} +232 -49
  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} +226 -49
  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
@@ -4,7 +4,8 @@ const {
4
4
  REDIS_V4,
5
5
  IOREDIS,
6
6
  REDIS_V3,
7
- } = require('./redisUtils')
7
+ } = require('../redisUtils')
8
+ const { HealthCheckCache } = require('./healthCheckCache')
8
9
 
9
10
  const HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000
10
11
 
@@ -68,7 +69,8 @@ function maskSensitiveData(text) {
68
69
  /**
69
70
  * @typedef {Object} ComponentHealth
70
71
  * @property {HealthStatus} status - Component health status
71
- * @property {string} [message] - Optional status message
72
+ * @property {string} [error] - Error message if status is unhealthy
73
+ * @property {string} [message] - Optional status message (deprecated, use error)
72
74
  * @property {number} [latencyMs] - Connection latency in milliseconds
73
75
  */
74
76
 
@@ -82,8 +84,8 @@ function maskSensitiveData(text) {
82
84
  * @typedef {Object} HealthCheckResult
83
85
  * @property {HealthStatus} status - Overall health status
84
86
  * @property {string} timestamp - ISO timestamp of the check
85
- * @property {boolean} cached - Whether this result is from cache
86
- * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health
87
+ * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} checks - Individual service health checks
88
+ * @property {string[]} [errors] - Top-level error messages (not related to specific services)
87
89
  */
88
90
 
89
91
  /**
@@ -116,12 +118,14 @@ class HealthCheckClient {
116
118
  * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL
117
119
  * @param {string} [options.databaseName='main'] - Name for the main database
118
120
  * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)
119
- * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)
121
+ * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis) - used for cache
122
+ * @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in results (for backend)
120
123
  * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)
121
124
  * @param {string} [options.appName] - Application name for logging
122
125
  */
123
126
  constructor(options = {}) {
124
127
  this.redisClient = options.redisClient || null
128
+ this.includeRedisCheck = options.includeRedisCheck || false
125
129
  this.cacheTtlMs = options.cacheTtlMs ?? HEALTH_CHECK_CACHE_TTL_MS
126
130
  this.appName =
127
131
  options.appName || process.env.BUILD_APP_NAME || 'unknown-app'
@@ -131,17 +135,29 @@ class HealthCheckClient {
131
135
  /** @type {CachedHealthResult | null} */
132
136
  this._cachedResult = null
133
137
 
138
+ /** @type {Promise<HealthCheckResult> | null} */
139
+ this._refreshPromise = null
140
+
134
141
  /** @type {Map<string, Pool>} */
135
142
  this._databasePools = new Map()
136
143
 
144
+ /** @type {DatabaseConfig | null} */
145
+ this._mainDatabaseConfig = null
146
+
137
147
  /** @type {DatabaseConfig[]} */
138
- this._databaseConfigs = []
148
+ this._clusterConfigs = []
139
149
 
140
150
  this._initDatabases(options)
141
151
 
142
152
  if (this.redisClient) {
143
153
  this._redisClientType = getRedisClientType(this.redisClient)
144
154
  }
155
+
156
+ this._cache = new HealthCheckCache({
157
+ redisClient: this.redisClient,
158
+ appName: this.appName,
159
+ cacheTtlMs: this.cacheTtlMs,
160
+ })
145
161
  }
146
162
 
147
163
  /**
@@ -151,16 +167,16 @@ class HealthCheckClient {
151
167
  */
152
168
  _initDatabases(options) {
153
169
  const mainUrl = options.databaseUrl || process.env.DATABASE_URL || ''
154
- const mainName = options.databaseName || 'main'
170
+ const mainName = options.databaseName || `${this.appName}_db`
155
171
 
156
172
  if (mainUrl) {
157
- this._databaseConfigs.push({ name: mainName, url: mainUrl })
173
+ this._mainDatabaseConfig = { name: mainName, url: mainUrl }
158
174
  }
159
175
 
160
176
  const additionalUrls = options.additionalDatabaseUrls || {}
161
177
  for (const [name, url] of Object.entries(additionalUrls)) {
162
178
  if (url) {
163
- this._databaseConfigs.push({ name, url })
179
+ this._clusterConfigs.push({ name, url })
164
180
  }
165
181
  }
166
182
  }
@@ -215,43 +231,71 @@ class HealthCheckClient {
215
231
  } catch (err) {
216
232
  return {
217
233
  status: 'unhealthy',
218
- message: maskSensitiveData(err.message),
234
+ error: maskSensitiveData(err.message),
219
235
  latencyMs: Date.now() - start,
220
236
  }
221
237
  }
222
238
  }
223
239
 
224
240
  /**
225
- * Tests all PostgreSQL database clusters in parallel.
226
- * @returns {Promise<DatabaseClusterHealth | null>}
241
+ * Tests all PostgreSQL databases (main + clusters) in parallel.
242
+ * @returns {Promise<Object | null>} Database health with optional clusters
227
243
  * @private
228
244
  */
229
245
  async _checkAllDatabases() {
230
- if (this._databaseConfigs.length === 0) {
246
+ if (!this._mainDatabaseConfig && this._clusterConfigs.length === 0) {
231
247
  return null
232
248
  }
233
249
 
234
- const results = await Promise.all(
235
- this._databaseConfigs.map(async config => ({
236
- name: config.name,
237
- health: await this._checkSingleDatabase(config),
238
- }))
239
- )
250
+ // Check main database
251
+ let mainHealth = null
252
+ if (this._mainDatabaseConfig) {
253
+ mainHealth = await this._checkSingleDatabase(this._mainDatabaseConfig)
254
+ }
255
+
256
+ // Check clusters in parallel
257
+ let clusters = null
258
+ if (this._clusterConfigs.length > 0) {
259
+ const clusterResults = await Promise.all(
260
+ this._clusterConfigs.map(async config => ({
261
+ name: config.name,
262
+ health: await this._checkSingleDatabase(config),
263
+ }))
264
+ )
265
+
266
+ clusters = {}
267
+ for (const { name, health } of clusterResults) {
268
+ clusters[name] = health
269
+ }
270
+ }
240
271
 
241
- const clusters = {}
242
- for (const { name, health } of results) {
243
- clusters[name] = health
272
+ // Calculate overall status
273
+ const allStatuses = []
274
+ if (mainHealth) allStatuses.push(mainHealth.status)
275
+ if (clusters) {
276
+ allStatuses.push(...Object.values(clusters).map(c => c.status))
244
277
  }
245
278
 
246
- const statuses = Object.values(clusters).map(c => c.status)
247
279
  let overallStatus = 'healthy'
248
- if (statuses.some(s => s === 'unhealthy')) {
280
+ if (allStatuses.some(s => s === 'unhealthy')) {
249
281
  overallStatus = 'unhealthy'
250
- } else if (statuses.some(s => s === 'degraded')) {
282
+ } else if (allStatuses.some(s => s === 'degraded')) {
251
283
  overallStatus = 'degraded'
252
284
  }
253
285
 
254
- return { status: overallStatus, clusters }
286
+ // Build result
287
+ const result = { status: overallStatus }
288
+ if (mainHealth) {
289
+ result.latencyMs = mainHealth.latencyMs
290
+ if (mainHealth.error) result.error = mainHealth.error
291
+ // Keep message for backward compatibility
292
+ if (mainHealth.message) result.message = mainHealth.message
293
+ }
294
+ if (clusters) {
295
+ result.clusters = clusters
296
+ }
297
+
298
+ return result
255
299
  }
256
300
 
257
301
  /**
@@ -309,24 +353,71 @@ class HealthCheckClient {
309
353
  /**
310
354
  * Performs a full health check on all configured components.
311
355
  * Results are cached for the configured TTL to prevent excessive load.
356
+ * Uses a mutex pattern to prevent concurrent health checks when cache expires.
357
+ * If cache is expired but a refresh is in progress, returns stale cache (if available).
312
358
  *
313
359
  * @returns {Promise<HealthCheckResult>}
314
360
  */
315
361
  async performHealthCheck() {
362
+ // If cache is valid, return immediately
316
363
  if (this._isCacheValid()) {
317
364
  return { ...this._cachedResult.result, cached: true }
318
365
  }
319
366
 
320
- const [dbHealth, redisHealth] = await Promise.all([
321
- this._checkAllDatabases(),
322
- this._checkRedis(),
323
- ])
367
+ // If a refresh is already in progress, return stale cache (if available) or wait for refresh
368
+ if (this._refreshPromise) {
369
+ // Return stale cache immediately if available, otherwise wait for refresh
370
+ if (this._cachedResult) {
371
+ return { ...this._cachedResult.result, cached: true }
372
+ }
373
+ // Wait for the in-progress refresh
374
+ return this._refreshPromise
375
+ }
376
+
377
+ // Start a new refresh
378
+ this._refreshPromise = this._performHealthCheckInternal()
379
+ .then(result => {
380
+ this._refreshPromise = null
381
+ return result
382
+ })
383
+ .catch(err => {
384
+ this._refreshPromise = null
385
+ throw err
386
+ })
387
+
388
+ // Return stale cache if available while refresh is happening, otherwise wait
389
+ if (this._cachedResult) {
390
+ // Return stale cache immediately, refresh will happen in background
391
+ return { ...this._cachedResult.result, cached: true }
392
+ }
324
393
 
325
- const components = {}
326
- if (dbHealth) components.database = dbHealth
327
- if (this.redisClient) components.redis = redisHealth
394
+ // No stale cache available, wait for refresh
395
+ return this._refreshPromise
396
+ }
328
397
 
329
- const statuses = Object.values(components).map(c => c.status)
398
+ /**
399
+ * Internal method that actually performs the health check.
400
+ * This is separated to allow the mutex pattern in performHealthCheck.
401
+ * Only checks database - Redis is used only for cache, not health check.
402
+ *
403
+ * @returns {Promise<HealthCheckResult>}
404
+ * @private
405
+ */
406
+ async _performHealthCheckInternal() {
407
+ // Check database
408
+ const dbHealth = await this._checkAllDatabases()
409
+
410
+ // Optionally check Redis (for backend)
411
+ let redisHealth = null
412
+ if (this.includeRedisCheck && this.redisClient) {
413
+ redisHealth = await this._checkRedis()
414
+ }
415
+
416
+ const checks = {}
417
+ if (dbHealth) checks.database = dbHealth
418
+ if (redisHealth) checks.redis = redisHealth
419
+
420
+ const statuses = Object.values(checks).map(c => c.status)
330
421
  let overallStatus = 'healthy'
331
422
 
332
423
  if (statuses.some(s => s === 'unhealthy')) {
@@ -339,8 +430,7 @@ class HealthCheckClient {
339
430
  const result = {
340
431
  status: overallStatus,
341
432
  timestamp: new Date().toISOString(),
342
- cached: false,
343
- components,
433
+ checks,
344
434
  }
345
435
 
346
436
  this._cachedResult = {
@@ -351,11 +441,59 @@ class HealthCheckClient {
351
441
  return result
352
442
  }
353
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
+
488
+ return result
489
+ }
490
+
354
491
  /**
355
492
  * Clears the cached health check result, forcing the next check to be fresh.
356
493
  */
357
494
  clearCache() {
358
495
  this._cachedResult = null
496
+ this._refreshPromise = null
359
497
  }
360
498
 
361
499
  /**
@@ -368,20 +506,29 @@ class HealthCheckClient {
368
506
  _getErrorMessages(result) {
369
507
  const errors = []
370
508
 
371
- if (result.components.database) {
372
- const db = result.components.database
509
+ if (result.checks.database) {
510
+ const db = result.checks.database
511
+
512
+ // Check main database status
513
+ if (db.status === 'unhealthy' && (db.message || db.error)) {
514
+ const dbName = this._mainDatabaseConfig?.name || 'main'
515
+ const errorMsg = db.error || db.message
516
+ errors.push(`DB ${dbName}: ${maskSensitiveData(errorMsg)}`)
517
+ }
518
+
519
+ // Check clusters
373
520
  if (db.clusters) {
374
521
  for (const [name, health] of Object.entries(db.clusters)) {
375
522
  if (health.status === 'unhealthy') {
376
- const message = health.message || 'connection failed'
523
+ const message = health.error || health.message || 'connection failed'
377
524
  errors.push(`DB ${name}: ${maskSensitiveData(message)}`)
378
525
  }
379
526
  }
380
527
  }
381
528
  }
382
529
 
383
- if (result.components.redis && result.components.redis.status === 'unhealthy') {
384
- const message = result.components.redis.message || 'connection failed'
530
+ if (result.checks.redis && result.checks.redis.status === 'unhealthy') {
531
+ const message = result.checks.redis.error || result.checks.redis.message || 'connection failed'
385
532
  errors.push(`Redis: ${maskSensitiveData(message)}`)
386
533
  }
387
534
 
@@ -394,17 +541,41 @@ class HealthCheckClient {
394
541
  * Response includes errors array when status is not healthy.
395
542
  * All sensitive data (passwords, connection strings, etc.) is masked.
396
543
  *
544
+ * This handler only reads from cache and never triggers database queries.
545
+ * Use a background worker to periodically refresh the cache.
546
+ *
397
547
  * @returns {(req: any, res: any) => Promise<void>} Express request handler
398
548
  */
399
549
  healthHandler() {
400
550
  return async (req, res) => {
401
551
  try {
402
- const result = await this.performHealthCheck()
552
+ // Only read from cache, never trigger DB queries
553
+ const result = await this.getCachedResult()
554
+
555
+ if (!result) {
556
+ // No cache available - return unhealthy status with proper format
557
+ const errorMsg = 'Health check cache not available. Worker may not be running.'
558
+ res.status(503).json({
559
+ status: 'unhealthy',
560
+ timestamp: new Date().toISOString(),
561
+ checks: {
562
+ redis: {
563
+ status: 'unhealthy',
564
+ error: errorMsg,
565
+ },
566
+ },
567
+ errors: [errorMsg],
568
+ })
569
+ return
570
+ }
571
+
403
572
  const statusCode = result.status === 'unhealthy' ? 503 : 200
404
-
405
- // Build response with errors if not healthy
573
+
574
+ // Build response - errors are already in result if present
406
575
  const response = { ...result }
407
- if (result.status !== 'healthy') {
576
+
577
+ // Add top-level errors array if not healthy and errors not already present
578
+ if (result.status !== 'healthy' && !result.errors) {
408
579
  const errors = this._getErrorMessages(result)
409
580
  if (errors.length > 0) {
410
581
  response.errors = errors
@@ -414,11 +585,17 @@ class HealthCheckClient {
414
585
  res.status(statusCode).json(response)
415
586
  } catch (err) {
416
587
  console.error(`${this.prefixLogs} Health check failed:`, err)
588
+ const errorMsg = maskSensitiveData(err.message)
417
589
  res.status(503).json({
418
590
  status: 'unhealthy',
419
591
  timestamp: new Date().toISOString(),
420
- cached: false,
421
- errors: [maskSensitiveData(err.message)],
592
+ checks: {
593
+ redis: {
594
+ status: 'unhealthy',
595
+ error: errorMsg,
596
+ },
597
+ },
598
+ errors: [errorMsg],
422
599
  })
423
600
  }
424
601
  }
@@ -428,9 +605,9 @@ class HealthCheckClient {
428
605
  * Register health check endpoint on an Express app.
429
606
  *
430
607
  * @param {import('express').Application} app - Express application
431
- * @param {string} [path='/health'] - Path for the health endpoint
608
+ * @param {string} [path='/health-status'] - Path for the health endpoint
432
609
  */
433
- registerHealthEndpoint(app, path = '/health') {
610
+ registerHealthEndpoint(app, path = '/health-status') {
434
611
  app.get(path, this.healthHandler())
435
612
  console.info(`${this.prefixLogs} Registered health endpoint at ${path}`)
436
613
  }
@@ -0,0 +1,143 @@
1
+ const redis = require('redis')
2
+ const { getRedisClientType, REDIS_V3, REDIS_V4 } = require('../redisUtils')
3
+
4
+ /**
5
+ * Creates a Redis client for health check cache with a specific client name.
6
+ * This allows the cache to be shared across worker and web processes.
7
+ *
8
+ * @param {Object} options
9
+ * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)
10
+ * @param {string} [options.appName] - Application name for client name
11
+ * @param {string} [options.clientName='healthcheck'] - Client name suffix
12
+ * @returns {any | null} Redis client instance or null if REDIS_URL not available
13
+ */
14
+ function createHealthCheckRedisClient(options = {}) {
15
+ const redisUrl = options.redisUrl || process.env.REDIS_URL
16
+ const appName = options.appName || process.env.METRICS_APP_NAME || process.env.BUILD_APP_NAME || 'unknown-app'
17
+ const clientName = options.clientName || 'healthcheck'
18
+
19
+ if (!redisUrl) {
20
+ console.warn(`[HealthCheck] REDIS_URL not configured for ${appName}, cache will be in-memory only (not shared across processes)`)
21
+ return null
22
+ }
23
+
24
+ try {
25
+ const client = redis.createClient({
26
+ url: redisUrl,
27
+ retry_strategy: retryOptions => {
28
+ if (retryOptions.attempt > 3) {
29
+ console.warn(`[HealthCheck] Redis client connection failed after max retries for ${appName}`)
30
+ return undefined
31
+ }
32
+ return 2000
33
+ },
34
+ })
35
+
36
+ // Set client name for identification
37
+ if (process.env.NODE_ENV !== 'test') {
38
+ client.on('connect', () => {
39
+ const fullClientName = `${appName}:${clientName}`
40
+ client.send_command(
41
+ 'CLIENT',
42
+ ['SETNAME', fullClientName],
43
+ err => {
44
+ if (err) {
45
+ console.error(`[HealthCheck] Failed to set client name for ${fullClientName}:`, err)
46
+ } else {
47
+ console.log(`[HealthCheck] Connected to Redis for pid:${process.pid} (${fullClientName})`)
48
+ }
49
+ }
50
+ )
51
+ })
52
+ }
53
+
54
+ client.on('error', err => {
55
+ console.warn(`[HealthCheck] Redis client error for ${appName}:`, err.message)
56
+ })
57
+
58
+ return client
59
+ } catch (err) {
60
+ console.warn(`[HealthCheck] Failed to create Redis client for ${appName}:`, err.message)
61
+ return null
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Creates a health check client for worker process.
67
+ * Worker needs database config to perform health checks and store results in Redis.
68
+ *
69
+ * @param {Object} options
70
+ * @param {string} options.databaseUrl - Main database URL
71
+ * @param {string} options.databaseName - Database name
72
+ * @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
73
+ * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
74
+ * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)
75
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
76
+ * @returns {HealthCheckClient} Configured health check client instance for worker
77
+ */
78
+ function createHealthCheckWorkerClient(options) {
79
+ const { HealthCheckClient } = require('./healthCheckClient')
80
+ const {
81
+ databaseUrl,
82
+ databaseName,
83
+ appName,
84
+ additionalDatabaseUrls,
85
+ redisUrl,
86
+ includeRedisCheck = false,
87
+ cacheTtlMs = 60000,
88
+ } = options
89
+
90
+ const redisClient = createHealthCheckRedisClient({
91
+ redisUrl,
92
+ appName,
93
+ clientName: 'healthcheck-worker',
94
+ })
95
+
96
+ const clientOptions = {
97
+ databaseUrl,
98
+ databaseName,
99
+ appName: appName || process.env.BUILD_APP_NAME || 'unknown-app',
100
+ redisClient,
101
+ includeRedisCheck,
102
+ cacheTtlMs,
103
+ }
104
+
105
+ if (additionalDatabaseUrls) {
106
+ clientOptions.additionalDatabaseUrls = additionalDatabaseUrls
107
+ }
108
+
109
+ return new HealthCheckClient(clientOptions)
110
+ }
111
+
112
+ /**
113
+ * Creates a health check client for endpoint handlers.
114
+ * Endpoints only read from cache, so they only need Redis client (no database config).
115
+ *
116
+ * @param {Object} options
117
+ * @param {any} options.redisClient - Existing Redis client instance (from system) - REQUIRED
118
+ * @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response (for backend)
119
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
120
+ * @returns {HealthCheckClient} Configured health check client instance for endpoints
121
+ */
122
+ function createHealthCheckEndpointClient(options) {
123
+ const { HealthCheckClient } = require('./healthCheckClient')
124
+ const { redisClient, includeRedisCheck = false, cacheTtlMs = 60000 } = options
125
+
126
+ if (!redisClient) {
127
+ throw new Error('redisClient is required for createHealthCheckEndpointClient')
128
+ }
129
+
130
+ return new HealthCheckClient({
131
+ appName: process.env.BUILD_APP_NAME || 'unknown-app',
132
+ redisClient,
133
+ cacheTtlMs,
134
+ includeRedisCheck,
135
+ })
136
+ }
137
+
138
+ module.exports = {
139
+ createHealthCheckRedisClient,
140
+ createHealthCheckWorkerClient,
141
+ createHealthCheckEndpointClient,
142
+ }
143
+
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared health check worker utility.
3
+ * This runs as a separate process and periodically refreshes the health check cache,
4
+ * preventing HTTP requests from triggering database queries.
5
+ * Creates its own Redis connection and health check client internally.
6
+ *
7
+ * @param {Object} options
8
+ * @param {string} options.databaseUrl - Main database URL
9
+ * @param {string} options.databaseName - Database name
10
+ * @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
11
+ * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
12
+ * @param {string} [options.redisUrl] - Redis URL (defaults to process.env.REDIS_URL)
13
+ * @param {number} [options.refreshIntervalMs=60000] - Refresh interval in milliseconds (default: 1 minute)
14
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
15
+ */
16
+ const { createHealthCheckWorkerClient } = require('./healthCheckUtils')
17
+
18
+ export function createHealthCheckWorker(options) {
19
+ const { refreshIntervalMs = 60000, ...workerClientOptions } = options
20
+
21
+ const healthCheckClient = createHealthCheckWorkerClient(workerClientOptions)
22
+
23
+ return async function runHealthCheckWorker() {
24
+ console.log('[HealthCheckWorker] Starting health check worker...')
25
+ console.log(`[HealthCheckWorker] Refresh interval: ${refreshIntervalMs}ms`)
26
+
27
+ try {
28
+ await healthCheckClient.refreshCache()
29
+ console.log('[HealthCheckWorker] Initial health check completed')
30
+ } catch (err) {
31
+ console.error('[HealthCheckWorker] Initial health check failed:', err)
32
+ }
33
+
34
+ const interval = setInterval(async () => {
35
+ try {
36
+ const result = await healthCheckClient.refreshCache()
37
+ console.log(
38
+ `[HealthCheckWorker] Health check refreshed at ${result.timestamp}, status: ${result.status}`
39
+ )
40
+ } catch (err) {
41
+ console.error('[HealthCheckWorker] Health check refresh failed:', err)
42
+ }
43
+ }, refreshIntervalMs)
44
+
45
+ process.on('SIGTERM', () => {
46
+ console.log('[HealthCheckWorker] Received SIGTERM, shutting down...')
47
+ clearInterval(interval)
48
+ healthCheckClient.cleanup().finally(() => {
49
+ process.exit(0)
50
+ })
51
+ })
52
+
53
+ process.on('SIGINT', () => {
54
+ console.log('[HealthCheckWorker] Received SIGINT, shutting down...')
55
+ clearInterval(interval)
56
+ healthCheckClient.cleanup().finally(() => {
57
+ process.exit(0)
58
+ })
59
+ })
60
+ }
61
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
- export * from './baseMetricsClient'
2
- export * from './metricsClient'
3
- export * from './metricsRedisClient'
4
- export * from './metricsQueueRedisClient'
5
- export * from './metricsDatabaseClient'
6
- export * from './healthCheckClient'
1
+ export * from './metrics/baseMetricsClient'
2
+ export * from './metrics/metricsClient'
3
+ export * from './metrics/metricsRedisClient'
4
+ export * from './metrics/metricsQueueRedisClient'
5
+ export * from './metrics/metricsDatabaseClient'
6
+ export * from './health/healthCheckClient'
7
+ export * from './health/healthCheckUtils'
8
+ export * from './health/healthCheckWorker'
7
9
  export * from './redisUtils'
@@ -1,6 +1,6 @@
1
1
  const Queue = require('bee-queue')
2
- const { RedisMetricsClient } = require('.')
3
- const { IOREDIS } = require('./redisUtils')
2
+ const { RedisMetricsClient } = require('./metricsRedisClient')
3
+ const { IOREDIS } = require('../redisUtils')
4
4
 
5
5
  /**
6
6
  * QueueRedisMetricsClient extends RedisMetricsClient to collect
@@ -4,7 +4,7 @@ const {
4
4
  REDIS_V4,
5
5
  IOREDIS,
6
6
  REDIS_V3,
7
- } = require('./redisUtils')
7
+ } = require('../redisUtils')
8
8
 
9
9
  const redisConnectionStableFields = ['name', 'flags', 'cmd']
10
10
  const redisConnectionFields = ['name', 'flags', 'tot-mem', 'cmd']