@adalo/metrics 0.1.123 → 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} +53 -11
- package/lib/health/healthCheckClient.d.ts.map +1 -0
- package/lib/{healthCheckClient.js → health/healthCheckClient.js} +171 -25
- 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} +169 -29
- 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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const {
|
|
2
|
+
getRedisClientType,
|
|
3
|
+
REDIS_V4,
|
|
4
|
+
IOREDIS,
|
|
5
|
+
REDIS_V3,
|
|
6
|
+
} = require('../redisUtils')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HealthCheckCache provides a shared cache layer for health check results.
|
|
10
|
+
* It uses Redis if available for cross-process sharing, with graceful fallback
|
|
11
|
+
* to in-memory cache if Redis is not configured or unavailable.
|
|
12
|
+
*/
|
|
13
|
+
class HealthCheckCache {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {any} [options.redisClient] - Redis client instance (optional)
|
|
17
|
+
* @param {string} [options.appName] - Application name for cache key
|
|
18
|
+
* @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.redisClient = options.redisClient || null
|
|
22
|
+
this.appName = options.appName || process.env.BUILD_APP_NAME || 'unknown-app'
|
|
23
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 60 * 1000
|
|
24
|
+
this.cacheKey = `healthcheck:${this.appName}`
|
|
25
|
+
|
|
26
|
+
/** In-memory fallback cache */
|
|
27
|
+
this._memoryCache = null
|
|
28
|
+
this._memoryCacheTimestamp = null
|
|
29
|
+
|
|
30
|
+
if (this.redisClient) {
|
|
31
|
+
this._redisClientType = getRedisClientType(this.redisClient)
|
|
32
|
+
this._redisAvailable = true
|
|
33
|
+
} else {
|
|
34
|
+
this._redisAvailable = false
|
|
35
|
+
console.warn(
|
|
36
|
+
`[HealthCheckCache] Redis not configured for ${this.appName}, using in-memory cache only (not shared across processes)`
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if Redis is available and working.
|
|
43
|
+
* @returns {Promise<boolean>}
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
async _checkRedisAvailable() {
|
|
47
|
+
if (!this.redisClient || !this._redisAvailable) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
let pong
|
|
53
|
+
if (this._redisClientType === REDIS_V3) {
|
|
54
|
+
pong = await new Promise((resolve, reject) => {
|
|
55
|
+
this.redisClient.ping((err, result) => {
|
|
56
|
+
if (err) reject(err)
|
|
57
|
+
else resolve(result)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
} else if (
|
|
61
|
+
this._redisClientType === REDIS_V4 ||
|
|
62
|
+
this._redisClientType === IOREDIS
|
|
63
|
+
) {
|
|
64
|
+
pong = await this.redisClient.ping()
|
|
65
|
+
} else {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
return pong === 'PONG'
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Redis not available
|
|
71
|
+
if (this._redisAvailable) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`[HealthCheckCache] Redis became unavailable: ${err.message}`
|
|
74
|
+
)
|
|
75
|
+
this._redisAvailable = false
|
|
76
|
+
}
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets cached health check result from Redis (if available) or in-memory cache.
|
|
83
|
+
* Throws error if Redis is configured but read fails (so caller can return proper error format).
|
|
84
|
+
* @returns {Promise<Object | null>} Cached result or null
|
|
85
|
+
* @throws {Error} If Redis is configured but read fails
|
|
86
|
+
*/
|
|
87
|
+
async get() {
|
|
88
|
+
// If Redis is configured, we MUST read from it (don't fall back to memory)
|
|
89
|
+
if (this.redisClient) {
|
|
90
|
+
try {
|
|
91
|
+
let cachedStr
|
|
92
|
+
if (this._redisClientType === REDIS_V3) {
|
|
93
|
+
cachedStr = await new Promise((resolve, reject) => {
|
|
94
|
+
this.redisClient.get(this.cacheKey, (err, result) => {
|
|
95
|
+
if (err) reject(err)
|
|
96
|
+
else resolve(result)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
} else if (
|
|
100
|
+
this._redisClientType === REDIS_V4 ||
|
|
101
|
+
this._redisClientType === IOREDIS
|
|
102
|
+
) {
|
|
103
|
+
cachedStr = await this.redisClient.get(this.cacheKey)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (cachedStr) {
|
|
107
|
+
try {
|
|
108
|
+
const cached = JSON.parse(cachedStr)
|
|
109
|
+
if (cached.result && cached.timestamp) {
|
|
110
|
+
const age = Date.now() - cached.timestamp
|
|
111
|
+
if (age < this.cacheTtlMs) {
|
|
112
|
+
// Also update in-memory cache as backup
|
|
113
|
+
this._memoryCache = cached.result
|
|
114
|
+
this._memoryCacheTimestamp = cached.timestamp
|
|
115
|
+
return cached.result
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (parseErr) {
|
|
119
|
+
console.warn(
|
|
120
|
+
`[HealthCheckCache] Failed to parse Redis cache:`,
|
|
121
|
+
parseErr.message
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// No cache in Redis - return null (worker may not have run yet)
|
|
126
|
+
return null
|
|
127
|
+
} catch (redisErr) {
|
|
128
|
+
// Redis read failed - throw error so caller can return proper error format
|
|
129
|
+
this._redisAvailable = false
|
|
130
|
+
throw new Error(`Redis cache read failed: ${redisErr.message}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// No Redis configured - fall back to in-memory cache
|
|
135
|
+
if (this._memoryCache && this._memoryCacheTimestamp) {
|
|
136
|
+
const age = Date.now() - this._memoryCacheTimestamp
|
|
137
|
+
if (age < this.cacheTtlMs) {
|
|
138
|
+
return this._memoryCache
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Sets cached health check result in Redis (if available) and in-memory.
|
|
147
|
+
* @param {Object} result - Health check result to cache
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
async set(result) {
|
|
151
|
+
const cacheData = {
|
|
152
|
+
result,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Update in-memory cache
|
|
157
|
+
this._memoryCache = result
|
|
158
|
+
this._memoryCacheTimestamp = cacheData.timestamp
|
|
159
|
+
|
|
160
|
+
// Try to update Redis if available
|
|
161
|
+
if (await this._checkRedisAvailable()) {
|
|
162
|
+
try {
|
|
163
|
+
const cacheStr = JSON.stringify(cacheData)
|
|
164
|
+
const ttlSeconds = Math.ceil(this.cacheTtlMs / 1000) + 10
|
|
165
|
+
|
|
166
|
+
if (this._redisClientType === REDIS_V3) {
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
this.redisClient.setex(
|
|
169
|
+
this.cacheKey,
|
|
170
|
+
ttlSeconds,
|
|
171
|
+
cacheStr,
|
|
172
|
+
(err) => {
|
|
173
|
+
if (err) reject(err)
|
|
174
|
+
else resolve()
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
} else if (
|
|
179
|
+
this._redisClientType === REDIS_V4 ||
|
|
180
|
+
this._redisClientType === IOREDIS
|
|
181
|
+
) {
|
|
182
|
+
await this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr)
|
|
183
|
+
}
|
|
184
|
+
} catch (redisErr) {
|
|
185
|
+
// Redis write failed, but in-memory cache is updated
|
|
186
|
+
console.warn(
|
|
187
|
+
`[HealthCheckCache] Redis write failed (in-memory cache updated):`,
|
|
188
|
+
redisErr.message
|
|
189
|
+
)
|
|
190
|
+
this._redisAvailable = false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clears the cache (both Redis and in-memory).
|
|
197
|
+
* @returns {Promise<void>}
|
|
198
|
+
*/
|
|
199
|
+
async clear() {
|
|
200
|
+
this._memoryCache = null
|
|
201
|
+
this._memoryCacheTimestamp = null
|
|
202
|
+
|
|
203
|
+
if (await this._checkRedisAvailable()) {
|
|
204
|
+
try {
|
|
205
|
+
if (this._redisClientType === REDIS_V3) {
|
|
206
|
+
await new Promise((resolve, reject) => {
|
|
207
|
+
this.redisClient.del(this.cacheKey, (err) => {
|
|
208
|
+
if (err) reject(err)
|
|
209
|
+
else resolve()
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
} else if (
|
|
213
|
+
this._redisClientType === REDIS_V4 ||
|
|
214
|
+
this._redisClientType === IOREDIS
|
|
215
|
+
) {
|
|
216
|
+
await this.redisClient.del(this.cacheKey)
|
|
217
|
+
}
|
|
218
|
+
} catch (redisErr) {
|
|
219
|
+
console.warn(
|
|
220
|
+
`[HealthCheckCache] Redis clear failed:`,
|
|
221
|
+
redisErr.message
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Checks if Redis is configured and available.
|
|
229
|
+
* @returns {boolean}
|
|
230
|
+
*/
|
|
231
|
+
isRedisAvailable() {
|
|
232
|
+
return this._redisAvailable && this.redisClient !== null
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { HealthCheckCache }
|
|
237
|
+
|
|
@@ -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,6 +135,9 @@ 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
|
|
|
@@ -145,6 +152,12 @@ class HealthCheckClient {
|
|
|
145
152
|
if (this.redisClient) {
|
|
146
153
|
this._redisClientType = getRedisClientType(this.redisClient)
|
|
147
154
|
}
|
|
155
|
+
|
|
156
|
+
this._cache = new HealthCheckCache({
|
|
157
|
+
redisClient: this.redisClient,
|
|
158
|
+
appName: this.appName,
|
|
159
|
+
cacheTtlMs: this.cacheTtlMs,
|
|
160
|
+
})
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
/**
|
|
@@ -218,7 +231,7 @@ class HealthCheckClient {
|
|
|
218
231
|
} catch (err) {
|
|
219
232
|
return {
|
|
220
233
|
status: 'unhealthy',
|
|
221
|
-
|
|
234
|
+
error: maskSensitiveData(err.message),
|
|
222
235
|
latencyMs: Date.now() - start,
|
|
223
236
|
}
|
|
224
237
|
}
|
|
@@ -274,6 +287,8 @@ class HealthCheckClient {
|
|
|
274
287
|
const result = { status: overallStatus }
|
|
275
288
|
if (mainHealth) {
|
|
276
289
|
result.latencyMs = mainHealth.latencyMs
|
|
290
|
+
if (mainHealth.error) result.error = mainHealth.error
|
|
291
|
+
// Keep message for backward compatibility
|
|
277
292
|
if (mainHealth.message) result.message = mainHealth.message
|
|
278
293
|
}
|
|
279
294
|
if (clusters) {
|
|
@@ -338,24 +353,71 @@ class HealthCheckClient {
|
|
|
338
353
|
/**
|
|
339
354
|
* Performs a full health check on all configured components.
|
|
340
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).
|
|
341
358
|
*
|
|
342
359
|
* @returns {Promise<HealthCheckResult>}
|
|
343
360
|
*/
|
|
344
361
|
async performHealthCheck() {
|
|
362
|
+
// If cache is valid, return immediately
|
|
345
363
|
if (this._isCacheValid()) {
|
|
346
364
|
return { ...this._cachedResult.result, cached: true }
|
|
347
365
|
}
|
|
348
366
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
}
|
|
353
393
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
394
|
+
// No stale cache available, wait for refresh
|
|
395
|
+
return this._refreshPromise
|
|
396
|
+
}
|
|
357
397
|
|
|
358
|
-
|
|
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)
|
|
359
421
|
let overallStatus = 'healthy'
|
|
360
422
|
|
|
361
423
|
if (statuses.some(s => s === 'unhealthy')) {
|
|
@@ -368,8 +430,7 @@ class HealthCheckClient {
|
|
|
368
430
|
const result = {
|
|
369
431
|
status: overallStatus,
|
|
370
432
|
timestamp: new Date().toISOString(),
|
|
371
|
-
|
|
372
|
-
components,
|
|
433
|
+
checks,
|
|
373
434
|
}
|
|
374
435
|
|
|
375
436
|
this._cachedResult = {
|
|
@@ -380,11 +441,59 @@ class HealthCheckClient {
|
|
|
380
441
|
return result
|
|
381
442
|
}
|
|
382
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
|
+
|
|
383
491
|
/**
|
|
384
492
|
* Clears the cached health check result, forcing the next check to be fresh.
|
|
385
493
|
*/
|
|
386
494
|
clearCache() {
|
|
387
495
|
this._cachedResult = null
|
|
496
|
+
this._refreshPromise = null
|
|
388
497
|
}
|
|
389
498
|
|
|
390
499
|
/**
|
|
@@ -397,28 +506,29 @@ class HealthCheckClient {
|
|
|
397
506
|
_getErrorMessages(result) {
|
|
398
507
|
const errors = []
|
|
399
508
|
|
|
400
|
-
if (result.
|
|
401
|
-
const db = result.
|
|
509
|
+
if (result.checks.database) {
|
|
510
|
+
const db = result.checks.database
|
|
402
511
|
|
|
403
512
|
// Check main database status
|
|
404
|
-
if (db.status === 'unhealthy' && db.message) {
|
|
513
|
+
if (db.status === 'unhealthy' && (db.message || db.error)) {
|
|
405
514
|
const dbName = this._mainDatabaseConfig?.name || 'main'
|
|
406
|
-
|
|
515
|
+
const errorMsg = db.error || db.message
|
|
516
|
+
errors.push(`DB ${dbName}: ${maskSensitiveData(errorMsg)}`)
|
|
407
517
|
}
|
|
408
518
|
|
|
409
519
|
// Check clusters
|
|
410
520
|
if (db.clusters) {
|
|
411
521
|
for (const [name, health] of Object.entries(db.clusters)) {
|
|
412
522
|
if (health.status === 'unhealthy') {
|
|
413
|
-
const message = health.message || 'connection failed'
|
|
523
|
+
const message = health.error || health.message || 'connection failed'
|
|
414
524
|
errors.push(`DB ${name}: ${maskSensitiveData(message)}`)
|
|
415
525
|
}
|
|
416
526
|
}
|
|
417
527
|
}
|
|
418
528
|
}
|
|
419
529
|
|
|
420
|
-
if (result.
|
|
421
|
-
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'
|
|
422
532
|
errors.push(`Redis: ${maskSensitiveData(message)}`)
|
|
423
533
|
}
|
|
424
534
|
|
|
@@ -431,17 +541,41 @@ class HealthCheckClient {
|
|
|
431
541
|
* Response includes errors array when status is not healthy.
|
|
432
542
|
* All sensitive data (passwords, connection strings, etc.) is masked.
|
|
433
543
|
*
|
|
544
|
+
* This handler only reads from cache and never triggers database queries.
|
|
545
|
+
* Use a background worker to periodically refresh the cache.
|
|
546
|
+
*
|
|
434
547
|
* @returns {(req: any, res: any) => Promise<void>} Express request handler
|
|
435
548
|
*/
|
|
436
549
|
healthHandler() {
|
|
437
550
|
return async (req, res) => {
|
|
438
551
|
try {
|
|
439
|
-
|
|
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
|
+
|
|
440
572
|
const statusCode = result.status === 'unhealthy' ? 503 : 200
|
|
441
|
-
|
|
442
|
-
// Build response
|
|
573
|
+
|
|
574
|
+
// Build response - errors are already in result if present
|
|
443
575
|
const response = { ...result }
|
|
444
|
-
|
|
576
|
+
|
|
577
|
+
// Add top-level errors array if not healthy and errors not already present
|
|
578
|
+
if (result.status !== 'healthy' && !result.errors) {
|
|
445
579
|
const errors = this._getErrorMessages(result)
|
|
446
580
|
if (errors.length > 0) {
|
|
447
581
|
response.errors = errors
|
|
@@ -451,11 +585,17 @@ class HealthCheckClient {
|
|
|
451
585
|
res.status(statusCode).json(response)
|
|
452
586
|
} catch (err) {
|
|
453
587
|
console.error(`${this.prefixLogs} Health check failed:`, err)
|
|
588
|
+
const errorMsg = maskSensitiveData(err.message)
|
|
454
589
|
res.status(503).json({
|
|
455
590
|
status: 'unhealthy',
|
|
456
591
|
timestamp: new Date().toISOString(),
|
|
457
|
-
|
|
458
|
-
|
|
592
|
+
checks: {
|
|
593
|
+
redis: {
|
|
594
|
+
status: 'unhealthy',
|
|
595
|
+
error: errorMsg,
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
errors: [errorMsg],
|
|
459
599
|
})
|
|
460
600
|
}
|
|
461
601
|
}
|