@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.
- package/lib/health/healthCheckCache.d.ts +57 -0
- package/lib/health/healthCheckCache.d.ts.map +1 -0
- package/lib/health/healthCheckCache.js +200 -0
- package/lib/health/healthCheckCache.js.map +1 -0
- package/lib/{healthCheckClient.d.ts → health/healthCheckClient.d.ts} +59 -15
- package/lib/health/healthCheckClient.d.ts.map +1 -0
- package/lib/{healthCheckClient.js → health/healthCheckClient.js} +232 -49
- package/lib/health/healthCheckClient.js.map +1 -0
- package/lib/health/healthCheckUtils.d.ts +54 -0
- package/lib/health/healthCheckUtils.d.ts.map +1 -0
- package/lib/health/healthCheckUtils.js +142 -0
- package/lib/health/healthCheckUtils.js.map +1 -0
- package/lib/health/healthCheckWorker.d.ts +2 -0
- package/lib/health/healthCheckWorker.d.ts.map +1 -0
- package/lib/health/healthCheckWorker.js +64 -0
- package/lib/health/healthCheckWorker.js.map +1 -0
- package/lib/index.d.ts +8 -6
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +28 -6
- package/lib/index.js.map +1 -1
- package/lib/metrics/baseMetricsClient.d.ts.map +1 -0
- package/lib/metrics/baseMetricsClient.js.map +1 -0
- package/lib/metrics/metricsClient.d.ts.map +1 -0
- package/lib/metrics/metricsClient.js.map +1 -0
- package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -0
- package/lib/metrics/metricsDatabaseClient.js.map +1 -0
- package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -0
- package/lib/{metricsQueueRedisClient.js → metrics/metricsQueueRedisClient.js} +2 -2
- package/lib/metrics/metricsQueueRedisClient.js.map +1 -0
- package/lib/metrics/metricsRedisClient.d.ts.map +1 -0
- package/lib/{metricsRedisClient.js → metrics/metricsRedisClient.js} +1 -1
- package/lib/metrics/metricsRedisClient.js.map +1 -0
- package/package.json +1 -1
- package/src/health/healthCheckCache.js +237 -0
- package/src/{healthCheckClient.js → health/healthCheckClient.js} +226 -49
- package/src/health/healthCheckUtils.js +143 -0
- package/src/health/healthCheckWorker.js +61 -0
- package/src/index.ts +8 -6
- package/src/{metricsQueueRedisClient.js → metrics/metricsQueueRedisClient.js} +2 -2
- package/src/{metricsRedisClient.js → metrics/metricsRedisClient.js} +1 -1
- package/lib/baseMetricsClient.d.ts.map +0 -1
- package/lib/baseMetricsClient.js.map +0 -1
- package/lib/healthCheckClient.d.ts.map +0 -1
- package/lib/healthCheckClient.js.map +0 -1
- package/lib/metricsClient.d.ts.map +0 -1
- package/lib/metricsClient.js.map +0 -1
- package/lib/metricsDatabaseClient.d.ts.map +0 -1
- package/lib/metricsDatabaseClient.js.map +0 -1
- package/lib/metricsQueueRedisClient.d.ts.map +0 -1
- package/lib/metricsQueueRedisClient.js.map +0 -1
- package/lib/metricsRedisClient.d.ts.map +0 -1
- package/lib/metricsRedisClient.js.map +0 -1
- /package/lib/{baseMetricsClient.d.ts → metrics/baseMetricsClient.d.ts} +0 -0
- /package/lib/{baseMetricsClient.js → metrics/baseMetricsClient.js} +0 -0
- /package/lib/{metricsClient.d.ts → metrics/metricsClient.d.ts} +0 -0
- /package/lib/{metricsClient.js → metrics/metricsClient.js} +0 -0
- /package/lib/{metricsDatabaseClient.d.ts → metrics/metricsDatabaseClient.d.ts} +0 -0
- /package/lib/{metricsDatabaseClient.js → metrics/metricsDatabaseClient.js} +0 -0
- /package/lib/{metricsQueueRedisClient.d.ts → metrics/metricsQueueRedisClient.d.ts} +0 -0
- /package/lib/{metricsRedisClient.d.ts → metrics/metricsRedisClient.d.ts} +0 -0
- /package/src/{baseMetricsClient.js → metrics/baseMetricsClient.js} +0 -0
- /package/src/{metricsClient.js → metrics/metricsClient.js} +0 -0
- /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('
|
|
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} [
|
|
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 {
|
|
86
|
-
* @property {
|
|
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.
|
|
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 ||
|
|
170
|
+
const mainName = options.databaseName || `${this.appName}_db`
|
|
155
171
|
|
|
156
172
|
if (mainUrl) {
|
|
157
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
|
226
|
-
* @returns {Promise<
|
|
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.
|
|
246
|
+
if (!this._mainDatabaseConfig && this._clusterConfigs.length === 0) {
|
|
231
247
|
return null
|
|
232
248
|
}
|
|
233
249
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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 (
|
|
280
|
+
if (allStatuses.some(s => s === 'unhealthy')) {
|
|
249
281
|
overallStatus = 'unhealthy'
|
|
250
|
-
} else if (
|
|
282
|
+
} else if (allStatuses.some(s => s === 'degraded')) {
|
|
251
283
|
overallStatus = 'degraded'
|
|
252
284
|
}
|
|
253
285
|
|
|
254
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
394
|
+
// No stale cache available, wait for refresh
|
|
395
|
+
return this._refreshPromise
|
|
396
|
+
}
|
|
328
397
|
|
|
329
|
-
|
|
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
|
-
|
|
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.
|
|
372
|
-
const db = result.
|
|
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.
|
|
384
|
-
const message = result.
|
|
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
|
-
|
|
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
|
|
573
|
+
|
|
574
|
+
// Build response - errors are already in result if present
|
|
406
575
|
const response = { ...result }
|
|
407
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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('
|
|
2
|
+
const { RedisMetricsClient } = require('./metricsRedisClient')
|
|
3
|
+
const { IOREDIS } = require('../redisUtils')
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* QueueRedisMetricsClient extends RedisMetricsClient to collect
|