@adalo/metrics 0.1.125 → 0.1.126
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/README-health.md +242 -0
- package/lib/health/healthCheckCache.d.ts.map +1 -1
- package/lib/health/healthCheckCache.js.map +1 -1
- package/lib/health/healthCheckClient.d.ts.map +1 -1
- package/lib/health/healthCheckClient.js.map +1 -1
- package/lib/health/healthCheckUtils.d.ts +14 -21
- package/lib/health/healthCheckUtils.d.ts.map +1 -1
- package/lib/health/healthCheckUtils.js +14 -77
- package/lib/health/healthCheckUtils.js.map +1 -1
- package/lib/health/healthCheckWorker.d.ts.map +1 -1
- package/lib/health/healthCheckWorker.js +21 -11
- package/lib/health/healthCheckWorker.js.map +1 -1
- package/package.json +1 -1
- package/src/health/healthCheckCache.js +9 -17
- package/src/health/healthCheckClient.js +19 -8
- package/src/health/healthCheckUtils.js +11 -76
- package/src/health/healthCheckWorker.js +26 -13
package/README-health.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Health Check Client
|
|
2
|
+
|
|
3
|
+
Provides health check endpoints for external monitoring services like BetterStack.
|
|
4
|
+
Uses a background worker to periodically check database connections and cache results in Redis,
|
|
5
|
+
preventing HTTP requests from triggering database queries.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
1. **Worker Process**: Runs as a separate process (defined in Procfile)
|
|
10
|
+
- Performs health checks every minute (configurable)
|
|
11
|
+
- Checks database connections (and optionally Redis)
|
|
12
|
+
- Stores results in Redis cache with 60-second TTL
|
|
13
|
+
- Creates its own Redis connection with name `{appName}:healthcheck-worker`
|
|
14
|
+
|
|
15
|
+
2. **Web Processes**: Handle HTTP requests
|
|
16
|
+
- Read health check results from Redis cache only
|
|
17
|
+
- Never trigger database queries
|
|
18
|
+
- Return cached data or error if Redis unavailable
|
|
19
|
+
- Use existing system Redis client
|
|
20
|
+
|
|
21
|
+
## Environment Variables
|
|
22
|
+
|
|
23
|
+
| Variable | Description | Default |
|
|
24
|
+
|----------|-------------|---------|
|
|
25
|
+
| `BUILD_APP_NAME` | Application name for logging and Redis client naming | `unknown-app` |
|
|
26
|
+
| `HOSTNAME` | Dyno/instance ID for logging | `unknown-dyno` |
|
|
27
|
+
| `BUILD_DYNO_PROCESS_TYPE` | Process type for logging | `health-check-worker` |
|
|
28
|
+
| `HEALTH_LOG_VALUES` | Enable logging of health check refresh status (`true`/`false`) | `false` |
|
|
29
|
+
| `REDIS_URL` | Redis connection URL for cache | - |
|
|
30
|
+
| `DATABASE_URL` | Main PostgreSQL connection URL (for backend worker) | - |
|
|
31
|
+
| `META_DB_URL` | Main PostgreSQL connection URL (for database worker) | - |
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Backend Service
|
|
36
|
+
|
|
37
|
+
**Endpoint Client** (`lib/healthCheck.ts`):
|
|
38
|
+
```typescript
|
|
39
|
+
import { createHealthCheckEndpointClient } from '@adalo/metrics'
|
|
40
|
+
import { redisClient } from './redis'
|
|
41
|
+
|
|
42
|
+
export const healthCheckClient = createHealthCheckEndpointClient({
|
|
43
|
+
redisClient,
|
|
44
|
+
includeRedisCheck: true,
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Worker** (`health-check-worker.ts`):
|
|
49
|
+
```typescript
|
|
50
|
+
import { createHealthCheckWorker } from '@adalo/metrics'
|
|
51
|
+
import { createNamedRedisClient } from './lib/redis'
|
|
52
|
+
|
|
53
|
+
const healthCheckRedisClient = createNamedRedisClient('healthcheck-worker')
|
|
54
|
+
|
|
55
|
+
const runHealthCheckWorker = createHealthCheckWorker({
|
|
56
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
57
|
+
databaseName: 'backend',
|
|
58
|
+
redisClient: healthCheckRedisClient,
|
|
59
|
+
includeRedisCheck: true,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
runHealthCheckWorker().catch(err => {
|
|
63
|
+
console.error('[HealthCheckWorker] Fatal error:', err)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Procfile**:
|
|
69
|
+
```
|
|
70
|
+
health-check-worker: node dist/health-check-worker.js
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Database Service
|
|
74
|
+
|
|
75
|
+
**Endpoint Client** (`lib/healthCheck.ts`):
|
|
76
|
+
```typescript
|
|
77
|
+
import { createHealthCheckEndpointClient } from '@adalo/metrics'
|
|
78
|
+
import { redisClient } from './cache/client'
|
|
79
|
+
|
|
80
|
+
export const healthCheckClient = createHealthCheckEndpointClient({
|
|
81
|
+
redisClient,
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Worker** (`workers/health-check-worker.ts`):
|
|
86
|
+
```typescript
|
|
87
|
+
import { createHealthCheckWorker } from '@adalo/metrics'
|
|
88
|
+
import { getClusterDatabaseUrls } from '../utils/clusterUrls'
|
|
89
|
+
import { createNamedRedisClient } from '../lib/cache/client'
|
|
90
|
+
|
|
91
|
+
const healthCheckRedisClient = createNamedRedisClient('healthcheck-worker')
|
|
92
|
+
|
|
93
|
+
const runHealthCheckWorker = createHealthCheckWorker({
|
|
94
|
+
databaseUrl: process.env.META_DB_URL,
|
|
95
|
+
databaseName: 'meta_db',
|
|
96
|
+
additionalDatabaseUrls: getClusterDatabaseUrls(),
|
|
97
|
+
redisClient: healthCheckRedisClient,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
runHealthCheckWorker().catch(err => {
|
|
101
|
+
console.error('[HealthCheckWorker] Fatal error:', err)
|
|
102
|
+
process.exit(1)
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Procfile**:
|
|
107
|
+
```
|
|
108
|
+
health-check-worker: node --enable-source-maps dist/workers/health-check-worker.js
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Endpoints
|
|
112
|
+
|
|
113
|
+
### Default Endpoint
|
|
114
|
+
|
|
115
|
+
By default, `registerHealthEndpoint()` registers at `/health`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
healthCheckClient.registerHealthEndpoint(app)
|
|
119
|
+
// Registers: GET /health
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Custom Endpoints
|
|
123
|
+
|
|
124
|
+
You can also register custom endpoints that use the same cached data:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Backend: /healthcheck (old endpoint, still works)
|
|
128
|
+
app.get('/healthcheck', healthCheckClient.healthHandler())
|
|
129
|
+
|
|
130
|
+
// Database: /hc (old endpoint, still works)
|
|
131
|
+
app.get('/hc', healthCheckClient.healthHandler())
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Response Format
|
|
135
|
+
|
|
136
|
+
### Healthy Response
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"status": "healthy",
|
|
140
|
+
"timestamp": "2026-01-23T08:27:57.926Z",
|
|
141
|
+
"checks": {
|
|
142
|
+
"database": {
|
|
143
|
+
"status": "healthy",
|
|
144
|
+
"latencyMs": 52
|
|
145
|
+
},
|
|
146
|
+
"redis": {
|
|
147
|
+
"status": "healthy",
|
|
148
|
+
"latencyMs": 1
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Unhealthy Response (Redis Cache Unavailable)
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"status": "unhealthy",
|
|
158
|
+
"timestamp": "2026-01-23T08:27:57.926Z",
|
|
159
|
+
"checks": {
|
|
160
|
+
"redis": {
|
|
161
|
+
"status": "unhealthy",
|
|
162
|
+
"error": "Redis cache read failed: connection refused"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"errors": ["Redis cache read failed: connection refused"]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Unhealthy Response (Database Issue)
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"status": "unhealthy",
|
|
173
|
+
"timestamp": "2026-01-23T08:27:57.926Z",
|
|
174
|
+
"checks": {
|
|
175
|
+
"database": {
|
|
176
|
+
"status": "unhealthy",
|
|
177
|
+
"error": "connection timeout",
|
|
178
|
+
"latencyMs": 5000
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"errors": ["DB backend: connection timeout"]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Status Codes
|
|
186
|
+
|
|
187
|
+
- `200` - healthy or degraded
|
|
188
|
+
- `503` - unhealthy (at least one component failed)
|
|
189
|
+
|
|
190
|
+
## Configuration Options
|
|
191
|
+
|
|
192
|
+
### Worker Options
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
createHealthCheckWorker({
|
|
196
|
+
databaseUrl: string, // Required: Main database URL
|
|
197
|
+
databaseName: string, // Required: Database name
|
|
198
|
+
redisClient: any, // Required: Redis client instance (created with createNamedRedisClient('healthcheck-worker'))
|
|
199
|
+
additionalDatabaseUrls?: object, // Optional: Additional clusters (database service)
|
|
200
|
+
includeRedisCheck?: boolean, // Optional: Include Redis in health check (default: false)
|
|
201
|
+
refreshIntervalMs?: number, // Optional: Refresh interval (default: 60000ms)
|
|
202
|
+
cacheTtlMs?: number, // Optional: Cache TTL (default: 60000ms)
|
|
203
|
+
appName?: string, // Optional: App name (defaults to BUILD_APP_NAME)
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Endpoint Client Options
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
createHealthCheckEndpointClient({
|
|
211
|
+
redisClient: any, // Required: Existing Redis client from system
|
|
212
|
+
includeRedisCheck?: boolean, // Optional: Include Redis in response (default: false)
|
|
213
|
+
cacheTtlMs?: number, // Optional: Cache TTL (default: 60000ms)
|
|
214
|
+
})
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Logging Format
|
|
218
|
+
|
|
219
|
+
Worker logs follow the same format as metrics:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
[health-check-worker] [app-name] [dyno-id] [HealthCheck] Starting health check worker...
|
|
223
|
+
[health-check-worker] [app-name] [dyno-id] [HealthCheck] Refresh interval: 60000ms
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
When `HEALTH_LOG_VALUES=true`, additional logs are printed:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
[health-check-worker] [app-name] [dyno-id] [HealthCheck] Initial health check completed
|
|
230
|
+
[health-check-worker] [app-name] [dyno-id] [HealthCheck] Health check refreshed at 2026-01-23T08:27:57.926Z, status: healthy
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Error logs are always printed regardless of `HEALTH_LOG_VALUES` setting.
|
|
234
|
+
|
|
235
|
+
## Benefits
|
|
236
|
+
|
|
237
|
+
- **No Concurrent DB Queries**: Only worker queries database, endpoints read from cache
|
|
238
|
+
- **Same Data Per Minute**: All requests get the same cached result within the TTL period
|
|
239
|
+
- **Redis Optional**: Works even if Redis is disabled (falls back to in-memory cache per process)
|
|
240
|
+
- **Graceful Degradation**: Returns proper error format if Redis unavailable
|
|
241
|
+
- **Multi-Process Support**: Shared cache across all web process instances via Redis
|
|
242
|
+
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckCache.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckCache.js"],"names":[],"mappings":"AAOA;;;;GAIG;AACH;IACE;;;;;OAKG;IACH;QAJyB,WAAW,GAAzB,GAAG;QACc,OAAO;QACP,UAAU;
|
|
1
|
+
{"version":3,"file":"healthCheckCache.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckCache.js"],"names":[],"mappings":"AAOA;;;;GAIG;AACH;IACE;;;;;OAKG;IACH;QAJyB,WAAW,GAAzB,GAAG;QACc,OAAO;QACP,UAAU;OAsBrC;IAnBC,iBAA8C;IAC9C,gBACgE;IAChE,mBAAiD;IACjD,iBAA6C;IAE7C,+BAA+B;IAC/B,kBAAwB;IACxB,2BAAiC;IAG/B,qCAA4D;IAC5D,yBAA2B;IAS/B;;;;OAIG;IACH,6BAiCC;IAED;;;;;OAKG;IACH,OAHa,QAAQ,MAAM,GAAG,IAAI,CAAC,CA2DlC;IAED;;;;OAIG;IACH,YAHW,MAAM,GACJ,QAAQ,IAAI,CAAC,CAwCzB;IAED;;;OAGG;IACH,SAFa,QAAQ,IAAI,CAAC,CAyBzB;IAED;;;OAGG;IACH,oBAFa,OAAO,CAInB;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckCache.js","names":["getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","require","HealthCheckCache","constructor","options","redisClient","appName","process","env","BUILD_APP_NAME","cacheTtlMs","cacheKey","_memoryCache","_memoryCacheTimestamp","_redisClientType","_redisAvailable","console","warn","_checkRedisAvailable","pong","Promise","resolve","reject","ping","err","result","message","get","cachedStr","cached","JSON","parse","timestamp","age","Date","now","parseErr","redisErr","Error","set","cacheData","cacheStr","stringify","ttlSeconds","Math","ceil","setex","clear","del","isRedisAvailable","module","exports"],"sources":["../../src/health/healthCheckCache.js"],"sourcesContent":["const {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('../redisUtils')\n\n/**\n * HealthCheckCache provides a shared cache layer for health check results.\n * It uses Redis if available for cross-process sharing, with graceful fallback\n * to in-memory cache if Redis is not configured or unavailable.\n */\nclass HealthCheckCache {\n /**\n * @param {Object} options\n * @param {any} [options.redisClient] - Redis client instance (optional)\n * @param {string} [options.appName] - Application name for cache key\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds\n */\n constructor(options = {}) {\n this.redisClient = options.redisClient || null\n this.appName = options.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n this.cacheTtlMs = options.cacheTtlMs ?? 60 * 1000\n this.cacheKey = `healthcheck:${this.appName}`\n \n /** In-memory fallback cache */\n this._memoryCache = null\n this._memoryCacheTimestamp = null\n\n if (this.redisClient) {\n this._redisClientType = getRedisClientType(this.redisClient)\n this._redisAvailable = true\n } else {\n this._redisAvailable = false\n console.warn(\n `[HealthCheckCache] Redis not configured for ${this.appName}, using in-memory cache only (not shared across processes)`\n )\n }\n }\n\n /**\n * Checks if Redis is available and working.\n * @returns {Promise<boolean>}\n * @private\n */\n async _checkRedisAvailable() {\n if (!this.redisClient || !this._redisAvailable) {\n return false\n }\n\n try {\n let pong\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 false\n }\n return pong === 'PONG'\n } catch (err) {\n // Redis not available\n if (this._redisAvailable) {\n console.warn(\n `[HealthCheckCache] Redis became unavailable: ${err.message}`\n )\n this._redisAvailable = false\n }\n return false\n }\n }\n\n /**\n * Gets cached health check result from Redis (if available) or in-memory cache.\n * Throws error if Redis is configured but read fails (so caller can return proper error format).\n * @returns {Promise<Object | null>} Cached result or null\n * @throws {Error} If Redis is configured but read fails\n */\n async get() {\n // If Redis is configured, we MUST read from it (don't fall back to memory)\n if (this.redisClient) {\n try {\n let cachedStr\n if (this._redisClientType === REDIS_V3) {\n cachedStr = await new Promise((resolve, reject) => {\n this.redisClient.get(this.cacheKey, (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 cachedStr = await this.redisClient.get(this.cacheKey)\n }\n\n if (cachedStr) {\n try {\n const cached = JSON.parse(cachedStr)\n if (cached.result && cached.timestamp) {\n const age = Date.now() - cached.timestamp\n if (age < this.cacheTtlMs) {\n // Also update in-memory cache as backup\n this._memoryCache = cached.result\n this._memoryCacheTimestamp = cached.timestamp\n return cached.result\n }\n }\n } catch (parseErr) {\n console.warn(\n `[HealthCheckCache] Failed to parse Redis cache:`,\n parseErr.message\n )\n }\n }\n // No cache in Redis - return null (worker may not have run yet)\n return null\n } catch (redisErr) {\n // Redis read failed - throw error so caller can return proper error format\n this._redisAvailable = false\n throw new Error(`Redis cache read failed: ${redisErr.message}`)\n }\n }\n\n // No Redis configured - fall back to in-memory cache\n if (this._memoryCache && this._memoryCacheTimestamp) {\n const age = Date.now() - this._memoryCacheTimestamp\n if (age < this.cacheTtlMs) {\n return this._memoryCache\n }\n }\n\n return null\n }\n\n /**\n * Sets cached health check result in Redis (if available) and in-memory.\n * @param {Object} result - Health check result to cache\n * @returns {Promise<void>}\n */\n async set(result) {\n const cacheData = {\n result,\n timestamp: Date.now(),\n }\n\n // Update in-memory cache\n this._memoryCache = result\n this._memoryCacheTimestamp = cacheData.timestamp\n\n // Try to update Redis if available\n if (await this._checkRedisAvailable()) {\n try {\n const cacheStr = JSON.stringify(cacheData)\n const ttlSeconds = Math.ceil(this.cacheTtlMs / 1000) + 10\n\n if (this._redisClientType === REDIS_V3) {\n await new Promise((resolve, reject) => {\n this.redisClient.setex(\n this.cacheKey,\n ttlSeconds,\n cacheStr,\n (err) => {\n if (err) reject(err)\n else resolve()\n }\n )\n })\n } else if (\n this._redisClientType === REDIS_V4 ||\n this._redisClientType === IOREDIS\n ) {\n await this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr)\n }\n } catch (redisErr) {\n // Redis write failed, but in-memory cache is updated\n console.warn(\n `[HealthCheckCache] Redis write failed (in-memory cache updated):`,\n redisErr.message\n )\n this._redisAvailable = false\n }\n }\n }\n\n /**\n * Clears the cache (both Redis and in-memory).\n * @returns {Promise<void>}\n */\n async clear() {\n this._memoryCache = null\n this._memoryCacheTimestamp = null\n\n if (await this._checkRedisAvailable()) {\n try {\n if (this._redisClientType === REDIS_V3) {\n await new Promise((resolve, reject) => {\n this.redisClient.del(this.cacheKey, (err) => {\n if (err) reject(err)\n else resolve()\n })\n })\n } else if (\n this._redisClientType === REDIS_V4 ||\n this._redisClientType === IOREDIS\n ) {\n await this.redisClient.del(this.cacheKey)\n }\n } catch (redisErr) {\n console.warn(\n `[HealthCheckCache] Redis clear failed:`,\n redisErr.message\n )\n }\n }\n }\n\n /**\n * Checks if Redis is configured and available.\n * @returns {boolean}\n */\n isRedisAvailable() {\n return this._redisAvailable && this.redisClient !== null\n }\n}\n\nmodule.exports = { HealthCheckCache }\n\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"healthCheckCache.js","names":["getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","require","HealthCheckCache","constructor","options","redisClient","appName","process","env","BUILD_APP_NAME","cacheTtlMs","cacheKey","_memoryCache","_memoryCacheTimestamp","_redisClientType","_redisAvailable","console","warn","_checkRedisAvailable","pong","Promise","resolve","reject","ping","err","result","message","get","cachedStr","cached","JSON","parse","timestamp","age","Date","now","parseErr","redisErr","Error","set","cacheData","cacheStr","stringify","ttlSeconds","Math","ceil","setex","clear","del","isRedisAvailable","module","exports"],"sources":["../../src/health/healthCheckCache.js"],"sourcesContent":["const {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('../redisUtils')\n\n/**\n * HealthCheckCache provides a shared cache layer for health check results.\n * It uses Redis if available for cross-process sharing, with graceful fallback\n * to in-memory cache if Redis is not configured or unavailable.\n */\nclass HealthCheckCache {\n /**\n * @param {Object} options\n * @param {any} [options.redisClient] - Redis client instance (optional)\n * @param {string} [options.appName] - Application name for cache key\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds\n */\n constructor(options = {}) {\n this.redisClient = options.redisClient || null\n this.appName =\n options.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n this.cacheTtlMs = options.cacheTtlMs ?? 60 * 1000\n this.cacheKey = `healthcheck:${this.appName}`\n\n /** In-memory fallback cache */\n this._memoryCache = null\n this._memoryCacheTimestamp = null\n\n if (this.redisClient) {\n this._redisClientType = getRedisClientType(this.redisClient)\n this._redisAvailable = true\n } else {\n this._redisAvailable = false\n console.warn(\n `[HealthCheckCache] Redis not configured for ${this.appName}, using in-memory cache only (not shared across processes)`\n )\n }\n }\n\n /**\n * Checks if Redis is available and working.\n * @returns {Promise<boolean>}\n * @private\n */\n async _checkRedisAvailable() {\n if (!this.redisClient || !this._redisAvailable) {\n return false\n }\n\n try {\n let pong\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 false\n }\n return pong === 'PONG'\n } catch (err) {\n // Redis not available\n if (this._redisAvailable) {\n console.warn(\n `[HealthCheckCache] Redis became unavailable: ${err.message}`\n )\n this._redisAvailable = false\n }\n return false\n }\n }\n\n /**\n * Gets cached health check result from Redis (if available) or in-memory cache.\n * Throws error if Redis is configured but read fails (so caller can return proper error format).\n * @returns {Promise<Object | null>} Cached result or null\n * @throws {Error} If Redis is configured but read fails\n */\n async get() {\n // If Redis is configured, we MUST read from it (don't fall back to memory)\n if (this.redisClient) {\n try {\n let cachedStr\n if (this._redisClientType === REDIS_V3) {\n cachedStr = await new Promise((resolve, reject) => {\n this.redisClient.get(this.cacheKey, (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 cachedStr = await this.redisClient.get(this.cacheKey)\n }\n\n if (cachedStr) {\n try {\n const cached = JSON.parse(cachedStr)\n if (cached.result && cached.timestamp) {\n const age = Date.now() - cached.timestamp\n if (age < this.cacheTtlMs) {\n // Also update in-memory cache as backup\n this._memoryCache = cached.result\n this._memoryCacheTimestamp = cached.timestamp\n return cached.result\n }\n }\n } catch (parseErr) {\n console.warn(\n `[HealthCheckCache] Failed to parse Redis cache:`,\n parseErr.message\n )\n }\n }\n // No cache in Redis - return null (worker may not have run yet)\n return null\n } catch (redisErr) {\n // Redis read failed - throw error so caller can return proper error format\n this._redisAvailable = false\n throw new Error(`Redis cache read failed: ${redisErr.message}`)\n }\n }\n\n // No Redis configured - fall back to in-memory cache\n if (this._memoryCache && this._memoryCacheTimestamp) {\n const age = Date.now() - this._memoryCacheTimestamp\n if (age < this.cacheTtlMs) {\n return this._memoryCache\n }\n }\n\n return null\n }\n\n /**\n * Sets cached health check result in Redis (if available) and in-memory.\n * @param {Object} result - Health check result to cache\n * @returns {Promise<void>}\n */\n async set(result) {\n const cacheData = {\n result,\n timestamp: Date.now(),\n }\n\n // Update in-memory cache\n this._memoryCache = result\n this._memoryCacheTimestamp = cacheData.timestamp\n\n // Try to update Redis if available\n if (await this._checkRedisAvailable()) {\n try {\n const cacheStr = JSON.stringify(cacheData)\n const ttlSeconds = Math.ceil(this.cacheTtlMs / 1000) + 10\n\n if (this._redisClientType === REDIS_V3) {\n await new Promise((resolve, reject) => {\n this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr, err => {\n if (err) reject(err)\n else resolve()\n })\n })\n } else if (\n this._redisClientType === REDIS_V4 ||\n this._redisClientType === IOREDIS\n ) {\n await this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr)\n }\n } catch (redisErr) {\n // Redis write failed, but in-memory cache is updated\n console.warn(\n `[HealthCheckCache] Redis write failed (in-memory cache updated):`,\n redisErr.message\n )\n this._redisAvailable = false\n }\n }\n }\n\n /**\n * Clears the cache (both Redis and in-memory).\n * @returns {Promise<void>}\n */\n async clear() {\n this._memoryCache = null\n this._memoryCacheTimestamp = null\n\n if (await this._checkRedisAvailable()) {\n try {\n if (this._redisClientType === REDIS_V3) {\n await new Promise((resolve, reject) => {\n this.redisClient.del(this.cacheKey, err => {\n if (err) reject(err)\n else resolve()\n })\n })\n } else if (\n this._redisClientType === REDIS_V4 ||\n this._redisClientType === IOREDIS\n ) {\n await this.redisClient.del(this.cacheKey)\n }\n } catch (redisErr) {\n console.warn(`[HealthCheckCache] Redis clear failed:`, redisErr.message)\n }\n }\n }\n\n /**\n * Checks if Redis is configured and available.\n * @returns {boolean}\n */\n isRedisAvailable() {\n return this._redisAvailable && this.redisClient !== null\n }\n}\n\nmodule.exports = { HealthCheckCache }\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckClient.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckClient.js"],"names":[],"mappings":"2BAoEa,SAAS,GAAG,WAAW,GAAG,UAAU;;;;;YAKnC,YAAY;;;;;;;;;;;;;;;;;;YAQZ,YAAY;;;;;YACL,MAAM,GAAE,eAAe;;;;;;;YAK9B,YAAY;;;;eACZ,MAAM;;;;;YACC,MAAM,GAAE,eAAe,GAAG,qBAAqB;;;;;;;;;;;YAMtD,iBAAiB;;;;eACjB,MAAM;;;;;;UAKN,MAAM;;;;SACN,MAAM;;AAnCpB;;GAEG;AAEH;;;;;;GAMG;AAEH;;;;GAIG;AAEH;;;;;;GAMG;AAEH;;;;GAIG;AAEH;;;;GAIG;AAEH;;;;;;;;;;;GAWG;AACH;IACE;;;;;;;;;OASG;IACH;QAR4B,WAAW;QACX,YAAY;QACI,sBAAsB;;;QACzC,WAAW,GAAzB,GAAG;QACe,iBAAiB;QAClB,UAAU;QACV,OAAO;OAqClC;IAlCC,iBAA8C;IAC9C,2BAA2D;IAC3D,mBAAiE;IACjE,gBACgE;IAEhE,mBAAmD;IAEnD,wCAAwC;IACxC,eADW,kBAAkB,GAAG,IAAI,CACX;IAEzB,gDAAgD;IAChD,iBADW,QAAQ,iBAAiB,CAAC,GAAG,IAAI,CACjB;IAE3B,gCAAgC;IAChC,gBADW,IAAI,MAAM,EAAE,IAAI,CAAC,CACG;IAE/B,oCAAoC;IACpC,qBADW,cAAc,GAAG,IAAI,CACD;IAE/B,+BAA+B;IAC/B,iBADW,cAAc,EAAE,CACF;IAKvB,qCAA4D;IAG9D,yBAIE;IAGJ;;;;OAIG;IACH,uBAcC;IAED;;;;;OAKG;IACH,iBAaC;IAED;;;;OAIG;IACH,sBAGC;IAED;;;;;OAKG;IACH,6BAiBC;IAED;;;;OAIG;IACH,2BAsDC;IAED;;;;OAIG;IACH,oBA6CC;IAED;;;;;;;OAOG;IACH,sBAFa,QAAQ,iBAAiB,CAAC,CAqCtC;IAED;;;;;;;OAOG;IACH,oCAoCC;IAED;;;;;;OAMG;IACH,mBAFa,QAAQ,iBAAiB,GAAG,IAAI,CAAC,CA0B7C;IAED;;;;;;OAMG;IACH,gBAFa,QAAQ,iBAAiB,CAAC,CAOtC;IAED;;OAEG;IACH,mBAGC;IAED;;;;;;OAMG;IACH,0BAkCC;IAED;;;;;;;;;;OAUG;IACH,uBAFmB,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,CAwDjD;IAED;;;;;OAKG;IACH,kEAGC;IAED;;;OAGG;IACH,WAFa,QAAQ,IAAI,CAAC,CAczB;CACF"}
|
|
@@ -1 +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":";;AAAA,MAAM;EAAEA;AAAK,CAAC,GAAGC,OAAO,CAAC,IAAI,CAAC;AAC9B,MAAM;EACJC,kBAAkB;EAClBC,QAAQ;EACRC,OAAO;EACPC;AACF,CAAC,GAAGJ,OAAO,CAAC,eAAe,CAAC;AAC5B,MAAM;EAAEK;AAAiB,CAAC,GAAGL,OAAO,CAAC,oBAAoB,CAAC;AAE1D,MAAMM,yBAAyB,GAAG,EAAE,GAAG,IAAI;;AAE3C;AACA;AACA;AACA,MAAMC,kBAAkB,GAAG;AACzB;AACA;EACEC,OAAO,EAAE,6FAA6F;EACtGC,WAAW,EAAE;AACf,CAAC;AACD;AACA;EACED,OAAO,EAAE,uCAAuC;EAChDC,WAAW,EAAE;AACf,CAAC;AACD;AACA;EACED,OAAO,EAAE,4FAA4F;EACrGC,WAAW,EAAE;AACf,CAAC;AACD;AACA;EACED,OAAO,EAAE,8EAA8E;EACvFC,WAAW,EAAE;AACf,CAAC;AACD;AACA;EACED,OAAO,EAAE,kDAAkD;EAC3DC,WAAW,EAAE;AACf,CAAC;AACD;AACA;EACED,OAAO,EAAE,uDAAuD;EAChEC,WAAW,EAAE;AACf,CAAC,CACF;;AAED;AACA;AACA;AACA;AACA;AACA,SAASC,iBAAiBA,CAACC,IAAI,EAAE;EAC/B,IAAI,CAACA,IAAI,IAAI,OAAOA,IAAI,KAAK,QAAQ,EAAE;IACrC,OAAOA,IAAI;EACb;EAEA,IAAIC,MAAM,GAAGD,IAAI;EACjB,KAAK,MAAM;IAAEH,OAAO;IAAEC;EAAY,CAAC,IAAIF,kBAAkB,EAAE;IACzDK,MAAM,GAAGA,MAAM,CAACC,OAAO,CAACL,OAAO,EAAEC,WAAW,CAAC;EAC/C;EACA,OAAOG,MAAM;AACf;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAME,iBAAiB,CAAC;EACtB;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAACC,OAAO,GAAG,CAAC,CAAC,EAAE;IACxB,IAAI,CAACC,WAAW,GAAGD,OAAO,CAACC,WAAW,IAAI,IAAI;IAC9C,IAAI,CAACC,iBAAiB,GAAGF,OAAO,CAACE,iBAAiB,IAAI,KAAK;IAC3D,IAAI,CAACC,UAAU,GAAGH,OAAO,CAACG,UAAU,IAAIb,yBAAyB;IACjE,IAAI,CAACc,OAAO,GACVJ,OAAO,CAACI,OAAO,IAAIC,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;IAEhE,IAAI,CAACC,UAAU,GAAG,IAAI,IAAI,CAACJ,OAAO,iBAAiB;;IAEnD;IACA,IAAI,CAACK,aAAa,GAAG,IAAI;;IAEzB;IACA,IAAI,CAACC,eAAe,GAAG,IAAI;;IAE3B;IACA,IAAI,CAACC,cAAc,GAAG,IAAIC,GAAG,CAAC,CAAC;;IAE/B;IACA,IAAI,CAACC,mBAAmB,GAAG,IAAI;;IAE/B;IACA,IAAI,CAACC,eAAe,GAAG,EAAE;IAEzB,IAAI,CAACC,cAAc,CAACf,OAAO,CAAC;IAE5B,IAAI,IAAI,CAACC,WAAW,EAAE;MACpB,IAAI,CAACe,gBAAgB,GAAG/B,kBAAkB,CAAC,IAAI,CAACgB,WAAW,CAAC;IAC9D;IAEA,IAAI,CAACgB,MAAM,GAAG,IAAI5B,gBAAgB,CAAC;MACjCY,WAAW,EAAE,IAAI,CAACA,WAAW;MAC7BG,OAAO,EAAE,IAAI,CAACA,OAAO;MACrBD,UAAU,EAAE,IAAI,CAACA;IACnB,CAAC,CAAC;EACJ;;EAEA;AACF;AACA;AACA;AACA;EACEY,cAAcA,CAACf,OAAO,EAAE;IACtB,MAAMkB,OAAO,GAAGlB,OAAO,CAACmB,WAAW,IAAId,OAAO,CAACC,GAAG,CAACc,YAAY,IAAI,EAAE;IACrE,MAAMC,QAAQ,GAAGrB,OAAO,CAACsB,YAAY,IAAI,GAAG,IAAI,CAAClB,OAAO,KAAK;IAE7D,IAAIc,OAAO,EAAE;MACX,IAAI,CAACL,mBAAmB,GAAG;QAAEU,IAAI,EAAEF,QAAQ;QAAEG,GAAG,EAAEN;MAAQ,CAAC;IAC7D;IAEA,MAAMO,cAAc,GAAGzB,OAAO,CAAC0B,sBAAsB,IAAI,CAAC,CAAC;IAC3D,KAAK,MAAM,CAACH,IAAI,EAAEC,GAAG,CAAC,IAAIG,MAAM,CAACC,OAAO,CAACH,cAAc,CAAC,EAAE;MACxD,IAAID,GAAG,EAAE;QACP,IAAI,CAACV,eAAe,CAACe,IAAI,CAAC;UAAEN,IAAI;UAAEC;QAAI,CAAC,CAAC;MAC1C;IACF;EACF;;EAEA;AACF;AACA;AACA;AACA;AACA;EACEM,QAAQA,CAACC,MAAM,EAAE;IACf,IAAI,CAAC,IAAI,CAACpB,cAAc,CAACqB,GAAG,CAACD,MAAM,CAACR,IAAI,CAAC,EAAE;MACzC,IAAI,CAACZ,cAAc,CAACsB,GAAG,CACrBF,MAAM,CAACR,IAAI,EACX,IAAIxC,IAAI,CAAC;QACPmD,gBAAgB,EAAEH,MAAM,CAACP,GAAG;QAC5BW,GAAG,EAAE,CAAC;QACNC,iBAAiB,EAAE,KAAK;QACxBC,uBAAuB,EAAE;MAC3B,CAAC,CACH,CAAC;IACH;IACA,OAAO,IAAI,CAAC1B,cAAc,CAAC2B,GAAG,CAACP,MAAM,CAACR,IAAI,CAAC;EAC7C;;EAEA;AACF;AACA;AACA;AACA;EACEgB,aAAaA,CAAA,EAAG;IACd,IAAI,CAAC,IAAI,CAAC9B,aAAa,EAAE,OAAO,KAAK;IACrC,OAAO+B,IAAI,CAACC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAChC,aAAa,CAACiC,SAAS,GAAG,IAAI,CAACvC,UAAU;EACpE;;EAEA;AACF;AACA;AACA;AACA;AACA;EACE,MAAMwC,oBAAoBA,CAACZ,MAAM,EAAE;IACjC,MAAMa,KAAK,GAAGJ,IAAI,CAACC,GAAG,CAAC,CAAC;IAExB,IAAI;MACF,MAAMI,IAAI,GAAG,IAAI,CAACf,QAAQ,CAACC,MAAM,CAAC;MAClC,MAAMc,IAAI,CAACC,KAAK,CAAC,UAAU,CAAC;MAC5B,OAAO;QACLC,MAAM,EAAE,SAAS;QACjBC,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH,CAAC,CAAC,OAAOK,GAAG,EAAE;MACZ,OAAO;QACLF,MAAM,EAAE,WAAW;QACnBG,KAAK,EAAExD,iBAAiB,CAACuD,GAAG,CAACE,OAAO,CAAC;QACrCH,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH;EACF;;EAEA;AACF;AACA;AACA;AACA;EACE,MAAMQ,kBAAkBA,CAAA,EAAG;IACzB,IAAI,CAAC,IAAI,CAACvC,mBAAmB,IAAI,IAAI,CAACC,eAAe,CAACuC,MAAM,KAAK,CAAC,EAAE;MAClE,OAAO,IAAI;IACb;;IAEA;IACA,IAAIC,UAAU,GAAG,IAAI;IACrB,IAAI,IAAI,CAACzC,mBAAmB,EAAE;MAC5ByC,UAAU,GAAG,MAAM,IAAI,CAACX,oBAAoB,CAAC,IAAI,CAAC9B,mBAAmB,CAAC;IACxE;;IAEA;IACA,IAAI0C,QAAQ,GAAG,IAAI;IACnB,IAAI,IAAI,CAACzC,eAAe,CAACuC,MAAM,GAAG,CAAC,EAAE;MACnC,MAAMG,cAAc,GAAG,MAAMC,OAAO,CAACC,GAAG,CACtC,IAAI,CAAC5C,eAAe,CAAC6C,GAAG,CAAC,MAAM5B,MAAM,KAAK;QACxCR,IAAI,EAAEQ,MAAM,CAACR,IAAI;QACjBqC,MAAM,EAAE,MAAM,IAAI,CAACjB,oBAAoB,CAACZ,MAAM;MAChD,CAAC,CAAC,CACJ,CAAC;MAEDwB,QAAQ,GAAG,CAAC,CAAC;MACb,KAAK,MAAM;QAAEhC,IAAI;QAAEqC;MAAO,CAAC,IAAIJ,cAAc,EAAE;QAC7CD,QAAQ,CAAChC,IAAI,CAAC,GAAGqC,MAAM;MACzB;IACF;;IAEA;IACA,MAAMC,WAAW,GAAG,EAAE;IACtB,IAAIP,UAAU,EAAEO,WAAW,CAAChC,IAAI,CAACyB,UAAU,CAACP,MAAM,CAAC;IACnD,IAAIQ,QAAQ,EAAE;MACZM,WAAW,CAAChC,IAAI,CAAC,GAAGF,MAAM,CAACmC,MAAM,CAACP,QAAQ,CAAC,CAACI,GAAG,CAACI,CAAC,IAAIA,CAAC,CAAChB,MAAM,CAAC,CAAC;IACjE;IAEA,IAAIiB,aAAa,GAAG,SAAS;IAC7B,IAAIH,WAAW,CAACI,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,WAAW,CAAC,EAAE;MAC5CF,aAAa,GAAG,WAAW;IAC7B,CAAC,MAAM,IAAIH,WAAW,CAACI,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,UAAU,CAAC,EAAE;MAClDF,aAAa,GAAG,UAAU;IAC5B;;IAEA;IACA,MAAMG,MAAM,GAAG;MAAEpB,MAAM,EAAEiB;IAAc,CAAC;IACxC,IAAIV,UAAU,EAAE;MACda,MAAM,CAACnB,SAAS,GAAGM,UAAU,CAACN,SAAS;MACvC,IAAIM,UAAU,CAACJ,KAAK,EAAEiB,MAAM,CAACjB,KAAK,GAAGI,UAAU,CAACJ,KAAK;MACrD;MACA,IAAII,UAAU,CAACH,OAAO,EAAEgB,MAAM,CAAChB,OAAO,GAAGG,UAAU,CAACH,OAAO;IAC7D;IACA,IAAII,QAAQ,EAAE;MACZY,MAAM,CAACZ,QAAQ,GAAGA,QAAQ;IAC5B;IAEA,OAAOY,MAAM;EACf;;EAEA;AACF;AACA;AACA;AACA;EACE,MAAMC,WAAWA,CAAA,EAAG;IAClB,IAAI,CAAC,IAAI,CAACnE,WAAW,EAAE;MACrB,OAAO;QAAE8C,MAAM,EAAE,SAAS;QAAEI,OAAO,EAAE;MAAiB,CAAC;IACzD;IAEA,MAAMP,KAAK,GAAGJ,IAAI,CAACC,GAAG,CAAC,CAAC;IAExB,IAAI;MACF,IAAI4B,IAAI;MAER,IAAI,IAAI,CAACrD,gBAAgB,KAAK5B,QAAQ,EAAE;QACtCiF,IAAI,GAAG,MAAM,IAAIZ,OAAO,CAAC,CAACa,OAAO,EAAEC,MAAM,KAAK;UAC5C,IAAI,CAACtE,WAAW,CAACuE,IAAI,CAAC,CAACvB,GAAG,EAAEkB,MAAM,KAAK;YACrC,IAAIlB,GAAG,EAAEsB,MAAM,CAACtB,GAAG,CAAC,MACfqB,OAAO,CAACH,MAAM,CAAC;UACtB,CAAC,CAAC;QACJ,CAAC,CAAC;MACJ,CAAC,MAAM,IACL,IAAI,CAACnD,gBAAgB,KAAK9B,QAAQ,IAClC,IAAI,CAAC8B,gBAAgB,KAAK7B,OAAO,EACjC;QACAkF,IAAI,GAAG,MAAM,IAAI,CAACpE,WAAW,CAACuE,IAAI,CAAC,CAAC;MACtC,CAAC,MAAM;QACL,OAAO;UAAEzB,MAAM,EAAE,WAAW;UAAEI,OAAO,EAAE;QAA4B,CAAC;MACtE;MAEA,IAAIkB,IAAI,KAAK,MAAM,EAAE;QACnB,OAAO;UACLtB,MAAM,EAAE,SAAS;UACjBC,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;QAC1B,CAAC;MACH;MAEA,OAAO;QACLG,MAAM,EAAE,WAAW;QACnBI,OAAO,EAAEzD,iBAAiB,CAAC,6BAA6B2E,IAAI,EAAE,CAAC;QAC/DrB,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH,CAAC,CAAC,OAAOK,GAAG,EAAE;MACZ,OAAO;QACLF,MAAM,EAAE,WAAW;QACnBI,OAAO,EAAEzD,iBAAiB,CAACuD,GAAG,CAACE,OAAO,CAAC;QACvCH,SAAS,EAAER,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGG;MAC1B,CAAC;IACH;EACF;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACE,MAAM6B,kBAAkBA,CAAA,EAAG;IACzB;IACA,IAAI,IAAI,CAAClC,aAAa,CAAC,CAAC,EAAE;MACxB,OAAO;QAAE,GAAG,IAAI,CAAC9B,aAAa,CAAC0D,MAAM;QAAEO,MAAM,EAAE;MAAK,CAAC;IACvD;;IAEA;IACA,IAAI,IAAI,CAAChE,eAAe,EAAE;MACxB;MACA,IAAI,IAAI,CAACD,aAAa,EAAE;QACtB,OAAO;UAAE,GAAG,IAAI,CAACA,aAAa,CAAC0D,MAAM;UAAEO,MAAM,EAAE;QAAK,CAAC;MACvD;MACA;MACA,OAAO,IAAI,CAAChE,eAAe;IAC7B;;IAEA;IACA,IAAI,CAACA,eAAe,GAAG,IAAI,CAACiE,2BAA2B,CAAC,CAAC,CACtDC,IAAI,CAACT,MAAM,IAAI;MACd,IAAI,CAACzD,eAAe,GAAG,IAAI;MAC3B,OAAOyD,MAAM;IACf,CAAC,CAAC,CACDU,KAAK,CAAC5B,GAAG,IAAI;MACZ,IAAI,CAACvC,eAAe,GAAG,IAAI;MAC3B,MAAMuC,GAAG;IACX,CAAC,CAAC;;IAEJ;IACA,IAAI,IAAI,CAACxC,aAAa,EAAE;MACtB;MACA,OAAO;QAAE,GAAG,IAAI,CAACA,aAAa,CAAC0D,MAAM;QAAEO,MAAM,EAAE;MAAK,CAAC;IACvD;;IAEA;IACA,OAAO,IAAI,CAAChE,eAAe;EAC7B;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACE,MAAMiE,2BAA2BA,CAAA,EAAG;IAClC;IACA,MAAMG,QAAQ,GAAG,MAAM,IAAI,CAAC1B,kBAAkB,CAAC,CAAC;;IAEhD;IACA,IAAI2B,WAAW,GAAG,IAAI;IACtB,IAAI,IAAI,CAAC7E,iBAAiB,IAAI,IAAI,CAACD,WAAW,EAAE;MAC9C8E,WAAW,GAAG,MAAM,IAAI,CAACX,WAAW,CAAC,CAAC;IACxC;IAEA,MAAMY,MAAM,GAAG,CAAC,CAAC;IACjB,IAAIF,QAAQ,EAAEE,MAAM,CAACC,QAAQ,GAAGH,QAAQ;IACxC,IAAIC,WAAW,EAAEC,MAAM,CAACE,KAAK,GAAGH,WAAW;IAE3C,MAAMI,QAAQ,GAAGxD,MAAM,CAACmC,MAAM,CAACkB,MAAM,CAAC,CAACrB,GAAG,CAACI,CAAC,IAAIA,CAAC,CAAChB,MAAM,CAAC;IACzD,IAAIiB,aAAa,GAAG,SAAS;IAE7B,IAAImB,QAAQ,CAAClB,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,WAAW,CAAC,EAAE;MACzCF,aAAa,GAAG,WAAW;IAC7B,CAAC,MAAM,IAAImB,QAAQ,CAAClB,IAAI,CAACC,CAAC,IAAIA,CAAC,KAAK,UAAU,CAAC,EAAE;MAC/CF,aAAa,GAAG,UAAU;IAC5B;;IAEA;IACA,MAAMG,MAAM,GAAG;MACbpB,MAAM,EAAEiB,aAAa;MACrBtB,SAAS,EAAE,IAAIF,IAAI,CAAC,CAAC,CAAC4C,WAAW,CAAC,CAAC;MACnCJ;IACF,CAAC;IAED,IAAI,CAACvE,aAAa,GAAG;MACnB0D,MAAM;MACNzB,SAAS,EAAEF,IAAI,CAACC,GAAG,CAAC;IACtB,CAAC;IAED,OAAO0B,MAAM;EACf;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;EACE,MAAMkB,eAAeA,CAAA,EAAG;IACtB,IAAI;MACF,MAAMX,MAAM,GAAG,MAAM,IAAI,CAACzD,MAAM,CAACqB,GAAG,CAAC,CAAC;MACtC,IAAIoC,MAAM,EAAE;QACV,OAAOA,MAAM;MACf;MACA;MACA,OAAO,IAAI;IACb,CAAC,CAAC,OAAOzB,GAAG,EAAE;MACZ;MACAqC,OAAO,CAACpC,KAAK,CAAC,GAAG,IAAI,CAAC1C,UAAU,6BAA6B,EAAEyC,GAAG,CAAC;MACnE,MAAMsC,YAAY,GAAG7F,iBAAiB,CAACuD,GAAG,CAACE,OAAO,IAAI,mBAAmB,CAAC;MAC1E,OAAO;QACLJ,MAAM,EAAE,WAAW;QACnBL,SAAS,EAAE,IAAIF,IAAI,CAAC,CAAC,CAAC4C,WAAW,CAAC,CAAC;QACnCJ,MAAM,EAAE;UACNE,KAAK,EAAE;YACLnC,MAAM,EAAE,WAAW;YACnBG,KAAK,EAAEqC;UACT;QACF,CAAC;QACDC,MAAM,EAAE,CAACD,YAAY;MACvB,CAAC;IACH;EACF;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;EACE,MAAME,YAAYA,CAAA,EAAG;IACnB,MAAMtB,MAAM,GAAG,MAAM,IAAI,CAACQ,2BAA2B,CAAC,CAAC;IACvD,MAAM,IAAI,CAAC1D,MAAM,CAACgB,GAAG,CAACkC,MAAM,CAAC;IAE7B,OAAOA,MAAM;EACf;;EAEA;AACF;AACA;EACEuB,UAAUA,CAAA,EAAG;IACX,IAAI,CAACjF,aAAa,GAAG,IAAI;IACzB,IAAI,CAACC,eAAe,GAAG,IAAI;EAC7B;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;EACEiF,iBAAiBA,CAACxB,MAAM,EAAE;IACxB,MAAMqB,MAAM,GAAG,EAAE;IAEjB,IAAIrB,MAAM,CAACa,MAAM,CAACC,QAAQ,EAAE;MAC1B,MAAMW,EAAE,GAAGzB,MAAM,CAACa,MAAM,CAACC,QAAQ;;MAEjC;MACA,IAAIW,EAAE,CAAC7C,MAAM,KAAK,WAAW,KAAK6C,EAAE,CAACzC,OAAO,IAAIyC,EAAE,CAAC1C,KAAK,CAAC,EAAE;QACzD,MAAM2C,MAAM,GAAG,IAAI,CAAChF,mBAAmB,EAAEU,IAAI,IAAI,MAAM;QACvD,MAAMuE,QAAQ,GAAGF,EAAE,CAAC1C,KAAK,IAAI0C,EAAE,CAACzC,OAAO;QACvCqC,MAAM,CAAC3D,IAAI,CAAC,MAAMgE,MAAM,KAAKnG,iBAAiB,CAACoG,QAAQ,CAAC,EAAE,CAAC;MAC7D;;MAEA;MACA,IAAIF,EAAE,CAACrC,QAAQ,EAAE;QACf,KAAK,MAAM,CAAChC,IAAI,EAAEqC,MAAM,CAAC,IAAIjC,MAAM,CAACC,OAAO,CAACgE,EAAE,CAACrC,QAAQ,CAAC,EAAE;UACxD,IAAIK,MAAM,CAACb,MAAM,KAAK,WAAW,EAAE;YACjC,MAAMI,OAAO,GAAGS,MAAM,CAACV,KAAK,IAAIU,MAAM,CAACT,OAAO,IAAI,mBAAmB;YACrEqC,MAAM,CAAC3D,IAAI,CAAC,MAAMN,IAAI,KAAK7B,iBAAiB,CAACyD,OAAO,CAAC,EAAE,CAAC;UAC1D;QACF;MACF;IACF;IAEA,IAAIgB,MAAM,CAACa,MAAM,CAACE,KAAK,IAAIf,MAAM,CAACa,MAAM,CAACE,KAAK,CAACnC,MAAM,KAAK,WAAW,EAAE;MACrE,MAAMI,OAAO,GAAGgB,MAAM,CAACa,MAAM,CAACE,KAAK,CAAChC,KAAK,IAAIiB,MAAM,CAACa,MAAM,CAACE,KAAK,CAAC/B,OAAO,IAAI,mBAAmB;MAC/FqC,MAAM,CAAC3D,IAAI,CAAC,UAAUnC,iBAAiB,CAACyD,OAAO,CAAC,EAAE,CAAC;IACrD;IAEA,OAAOqC,MAAM;EACf;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACEO,aAAaA,CAAA,EAAG;IACd,OAAO,OAAOC,GAAG,EAAEC,GAAG,KAAK;MACzB,IAAI;QACF;QACA,MAAM9B,MAAM,GAAG,MAAM,IAAI,CAACkB,eAAe,CAAC,CAAC;QAE3C,IAAI,CAAClB,MAAM,EAAE;UACX;UACA,MAAM2B,QAAQ,GAAG,8DAA8D;UAC/EG,GAAG,CAAClD,MAAM,CAAC,GAAG,CAAC,CAACmD,IAAI,CAAC;YACnBnD,MAAM,EAAE,WAAW;YACnBL,SAAS,EAAE,IAAIF,IAAI,CAAC,CAAC,CAAC4C,WAAW,CAAC,CAAC;YACnCJ,MAAM,EAAE;cACNE,KAAK,EAAE;gBACLnC,MAAM,EAAE,WAAW;gBACnBG,KAAK,EAAE4C;cACT;YACF,CAAC;YACDN,MAAM,EAAE,CAACM,QAAQ;UACnB,CAAC,CAAC;UACF;QACF;QAEA,MAAMK,UAAU,GAAGhC,MAAM,CAACpB,MAAM,KAAK,WAAW,GAAG,GAAG,GAAG,GAAG;;QAE5D;QACA,MAAMqD,QAAQ,GAAG;UAAE,GAAGjC;QAAO,CAAC;;QAE9B;QACA,IAAIA,MAAM,CAACpB,MAAM,KAAK,SAAS,IAAI,CAACoB,MAAM,CAACqB,MAAM,EAAE;UACjD,MAAMA,MAAM,GAAG,IAAI,CAACG,iBAAiB,CAACxB,MAAM,CAAC;UAC7C,IAAIqB,MAAM,CAACnC,MAAM,GAAG,CAAC,EAAE;YACrB+C,QAAQ,CAACZ,MAAM,GAAGA,MAAM;UAC1B;QACF;QAEAS,GAAG,CAAClD,MAAM,CAACoD,UAAU,CAAC,CAACD,IAAI,CAACE,QAAQ,CAAC;MACvC,CAAC,CAAC,OAAOnD,GAAG,EAAE;QACZqC,OAAO,CAACpC,KAAK,CAAC,GAAG,IAAI,CAAC1C,UAAU,uBAAuB,EAAEyC,GAAG,CAAC;QAC7D,MAAM6C,QAAQ,GAAGpG,iBAAiB,CAACuD,GAAG,CAACE,OAAO,CAAC;QAC/C8C,GAAG,CAAClD,MAAM,CAAC,GAAG,CAAC,CAACmD,IAAI,CAAC;UACnBnD,MAAM,EAAE,WAAW;UACnBL,SAAS,EAAE,IAAIF,IAAI,CAAC,CAAC,CAAC4C,WAAW,CAAC,CAAC;UACnCJ,MAAM,EAAE;YACNE,KAAK,EAAE;cACLnC,MAAM,EAAE,WAAW;cACnBG,KAAK,EAAE4C;YACT;UACF,CAAC;UACDN,MAAM,EAAE,CAACM,QAAQ;QACnB,CAAC,CAAC;MACJ;IACF,CAAC;EACH;;EAEA;AACF;AACA;AACA;AACA;AACA;EACEO,sBAAsBA,CAACC,GAAG,EAAEC,IAAI,GAAG,SAAS,EAAE;IAC5CD,GAAG,CAAChE,GAAG,CAACiE,IAAI,EAAE,IAAI,CAACR,aAAa,CAAC,CAAC,CAAC;IACnCT,OAAO,CAACkB,IAAI,CAAC,GAAG,IAAI,CAAChG,UAAU,kCAAkC+F,IAAI,EAAE,CAAC;EAC1E;;EAEA;AACF;AACA;AACA;EACE,MAAME,OAAOA,CAAA,EAAG;IACd,KAAK,MAAM,CAAClF,IAAI,EAAEsB,IAAI,CAAC,IAAI,IAAI,CAAClC,cAAc,EAAE;MAC9C,IAAI;QACF,MAAMkC,IAAI,CAAC6D,GAAG,CAAC,CAAC;MAClB,CAAC,CAAC,OAAOzD,GAAG,EAAE;QACZqC,OAAO,CAACpC,KAAK,CAAC,GAAG,IAAI,CAAC1C,UAAU,gCAAgCe,IAAI,GAAG,EAAE0B,GAAG,CAAC;MAC/E;IACF;IACA,IAAI,CAACtC,cAAc,CAACgG,KAAK,CAAC,CAAC;EAC7B;AACF;AAEAC,MAAM,CAACC,OAAO,GAAG;EAAE/G;AAAkB,CAAC","ignoreList":[]}
|
|
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:\n /(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:\n /(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:\n /(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 =\n 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 =\n result.checks.redis.error ||\n result.checks.redis.message ||\n '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 =\n '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(\n `${this.prefixLogs} Error closing database pool ${name}:`,\n err\n )\n }\n }\n this._databasePools.clear()\n }\n}\n\nmodule.exports = { HealthCheckClient }\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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;
|
|
1
|
+
declare namespace _default {
|
|
2
|
+
export { createHealthCheckWorkerClient };
|
|
3
|
+
export { createHealthCheckEndpointClient };
|
|
4
|
+
}
|
|
5
|
+
export default _default;
|
|
16
6
|
/**
|
|
17
7
|
* Creates a health check client for worker process.
|
|
18
8
|
* Worker needs database config to perform health checks and store results in Redis.
|
|
@@ -20,22 +10,24 @@ export function createHealthCheckRedisClient(options?: {
|
|
|
20
10
|
* @param {Object} options
|
|
21
11
|
* @param {string} options.databaseUrl - Main database URL
|
|
22
12
|
* @param {string} options.databaseName - Database name
|
|
13
|
+
* @param {any} options.redisClient - Redis client instance (from service config with appropriate name)
|
|
23
14
|
* @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
|
|
24
15
|
* @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
|
|
25
|
-
* @param {
|
|
16
|
+
* @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response
|
|
26
17
|
* @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
|
|
27
18
|
* @returns {HealthCheckClient} Configured health check client instance for worker
|
|
28
19
|
*/
|
|
29
|
-
|
|
20
|
+
declare function createHealthCheckWorkerClient(options: {
|
|
30
21
|
databaseUrl: string;
|
|
31
22
|
databaseName: string;
|
|
23
|
+
redisClient: any;
|
|
32
24
|
appName?: string | undefined;
|
|
33
25
|
additionalDatabaseUrls?: {
|
|
34
26
|
[x: string]: string;
|
|
35
27
|
} | undefined;
|
|
36
|
-
|
|
28
|
+
includeRedisCheck?: boolean | undefined;
|
|
37
29
|
cacheTtlMs?: number | undefined;
|
|
38
|
-
}):
|
|
30
|
+
}): HealthCheckClient;
|
|
39
31
|
/**
|
|
40
32
|
* Creates a health check client for endpoint handlers.
|
|
41
33
|
* Endpoints only read from cache, so they only need Redis client (no database config).
|
|
@@ -46,9 +38,10 @@ export function createHealthCheckWorkerClient(options: {
|
|
|
46
38
|
* @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
|
|
47
39
|
* @returns {HealthCheckClient} Configured health check client instance for endpoints
|
|
48
40
|
*/
|
|
49
|
-
|
|
41
|
+
declare function createHealthCheckEndpointClient(options: {
|
|
50
42
|
redisClient: any;
|
|
51
43
|
includeRedisCheck?: boolean | undefined;
|
|
52
44
|
cacheTtlMs?: number | undefined;
|
|
53
|
-
}):
|
|
45
|
+
}): HealthCheckClient;
|
|
46
|
+
import { HealthCheckClient } from "./healthCheckClient";
|
|
54
47
|
//# sourceMappingURL=healthCheckUtils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckUtils.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckUtils.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"healthCheckUtils.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckUtils.js"],"names":[],"mappings":";;;;;AAEA;;;;;;;;;;;;;GAaG;AACH;IAT2B,WAAW,EAA3B,MAAM;IACU,YAAY,EAA5B,MAAM;IACO,WAAW,EAAxB,GAAG;IACc,OAAO;IACS,sBAAsB;;;IACrC,iBAAiB;IAClB,UAAU;IACzB,iBAAiB,CA+B7B;AAED;;;;;;;;;GASG;AACH;IALwB,WAAW,EAAxB,GAAG;IACe,iBAAiB;IAClB,UAAU;IACzB,iBAAiB,CAiB7B"}
|
|
@@ -1,65 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _healthCheckClient = require("./healthCheckClient");
|
|
63
8
|
/**
|
|
64
9
|
* Creates a health check client for worker process.
|
|
65
10
|
* Worker needs database config to perform health checks and store results in Redis.
|
|
@@ -67,30 +12,26 @@ function createHealthCheckRedisClient(options = {}) {
|
|
|
67
12
|
* @param {Object} options
|
|
68
13
|
* @param {string} options.databaseUrl - Main database URL
|
|
69
14
|
* @param {string} options.databaseName - Database name
|
|
15
|
+
* @param {any} options.redisClient - Redis client instance (from service config with appropriate name)
|
|
70
16
|
* @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
|
|
71
17
|
* @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
|
|
72
|
-
* @param {
|
|
18
|
+
* @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response
|
|
73
19
|
* @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
|
|
74
20
|
* @returns {HealthCheckClient} Configured health check client instance for worker
|
|
75
21
|
*/
|
|
76
22
|
function createHealthCheckWorkerClient(options) {
|
|
77
|
-
const {
|
|
78
|
-
HealthCheckClient
|
|
79
|
-
} = require('./healthCheckClient');
|
|
80
23
|
const {
|
|
81
24
|
databaseUrl,
|
|
82
25
|
databaseName,
|
|
26
|
+
redisClient,
|
|
83
27
|
appName,
|
|
84
28
|
additionalDatabaseUrls,
|
|
85
|
-
redisUrl,
|
|
86
29
|
includeRedisCheck = false,
|
|
87
30
|
cacheTtlMs = 60000
|
|
88
31
|
} = options;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
clientName: 'healthcheck-worker'
|
|
93
|
-
});
|
|
32
|
+
if (!redisClient) {
|
|
33
|
+
throw new Error('redisClient is required for createHealthCheckWorkerClient');
|
|
34
|
+
}
|
|
94
35
|
const clientOptions = {
|
|
95
36
|
databaseUrl,
|
|
96
37
|
databaseName,
|
|
@@ -102,7 +43,7 @@ function createHealthCheckWorkerClient(options) {
|
|
|
102
43
|
if (additionalDatabaseUrls) {
|
|
103
44
|
clientOptions.additionalDatabaseUrls = additionalDatabaseUrls;
|
|
104
45
|
}
|
|
105
|
-
return new HealthCheckClient(clientOptions);
|
|
46
|
+
return new _healthCheckClient.HealthCheckClient(clientOptions);
|
|
106
47
|
}
|
|
107
48
|
|
|
108
49
|
/**
|
|
@@ -116,9 +57,6 @@ function createHealthCheckWorkerClient(options) {
|
|
|
116
57
|
* @returns {HealthCheckClient} Configured health check client instance for endpoints
|
|
117
58
|
*/
|
|
118
59
|
function createHealthCheckEndpointClient(options) {
|
|
119
|
-
const {
|
|
120
|
-
HealthCheckClient
|
|
121
|
-
} = require('./healthCheckClient');
|
|
122
60
|
const {
|
|
123
61
|
redisClient,
|
|
124
62
|
includeRedisCheck = false,
|
|
@@ -127,15 +65,14 @@ function createHealthCheckEndpointClient(options) {
|
|
|
127
65
|
if (!redisClient) {
|
|
128
66
|
throw new Error('redisClient is required for createHealthCheckEndpointClient');
|
|
129
67
|
}
|
|
130
|
-
return new HealthCheckClient({
|
|
68
|
+
return new _healthCheckClient.HealthCheckClient({
|
|
131
69
|
appName: process.env.BUILD_APP_NAME || 'unknown-app',
|
|
132
70
|
redisClient,
|
|
133
71
|
cacheTtlMs,
|
|
134
72
|
includeRedisCheck
|
|
135
73
|
});
|
|
136
74
|
}
|
|
137
|
-
|
|
138
|
-
createHealthCheckRedisClient,
|
|
75
|
+
var _default = exports.default = {
|
|
139
76
|
createHealthCheckWorkerClient,
|
|
140
77
|
createHealthCheckEndpointClient
|
|
141
78
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckUtils.js","names":["
|
|
1
|
+
{"version":3,"file":"healthCheckUtils.js","names":["_healthCheckClient","require","createHealthCheckWorkerClient","options","databaseUrl","databaseName","redisClient","appName","additionalDatabaseUrls","includeRedisCheck","cacheTtlMs","Error","clientOptions","process","env","BUILD_APP_NAME","HealthCheckClient","createHealthCheckEndpointClient","_default","exports","default"],"sources":["../../src/health/healthCheckUtils.js"],"sourcesContent":["import { HealthCheckClient } from './healthCheckClient'\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 {any} options.redisClient - Redis client instance (from service config with appropriate 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 {boolean} [options.includeRedisCheck=false] - Include Redis health check in response\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 {\n databaseUrl,\n databaseName,\n redisClient,\n appName,\n additionalDatabaseUrls,\n includeRedisCheck = false,\n cacheTtlMs = 60000,\n } = options\n\n if (!redisClient) {\n throw new Error('redisClient is required for createHealthCheckWorkerClient')\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 { redisClient, includeRedisCheck = false, cacheTtlMs = 60000 } = options\n\n if (!redisClient) {\n throw new Error(\n 'redisClient is required for createHealthCheckEndpointClient'\n )\n }\n\n return new HealthCheckClient({\n appName: process.env.BUILD_APP_NAME || 'unknown-app',\n redisClient,\n cacheTtlMs,\n includeRedisCheck,\n })\n}\n\nexport default {\n createHealthCheckWorkerClient,\n createHealthCheckEndpointClient,\n}\n"],"mappings":";;;;;;AAAA,IAAAA,kBAAA,GAAAC,OAAA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,6BAA6BA,CAACC,OAAO,EAAE;EAC9C,MAAM;IACJC,WAAW;IACXC,YAAY;IACZC,WAAW;IACXC,OAAO;IACPC,sBAAsB;IACtBC,iBAAiB,GAAG,KAAK;IACzBC,UAAU,GAAG;EACf,CAAC,GAAGP,OAAO;EAEX,IAAI,CAACG,WAAW,EAAE;IAChB,MAAM,IAAIK,KAAK,CAAC,2DAA2D,CAAC;EAC9E;EAEA,MAAMC,aAAa,GAAG;IACpBR,WAAW;IACXC,YAAY;IACZE,OAAO,EAAEA,OAAO,IAAIM,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;IAC/DT,WAAW;IACXG,iBAAiB;IACjBC;EACF,CAAC;EAED,IAAIF,sBAAsB,EAAE;IAC1BI,aAAa,CAACJ,sBAAsB,GAAGA,sBAAsB;EAC/D;EAEA,OAAO,IAAIQ,oCAAiB,CAACJ,aAAa,CAAC;AAC7C;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASK,+BAA+BA,CAACd,OAAO,EAAE;EAChD,MAAM;IAAEG,WAAW;IAAEG,iBAAiB,GAAG,KAAK;IAAEC,UAAU,GAAG;EAAM,CAAC,GAAGP,OAAO;EAE9E,IAAI,CAACG,WAAW,EAAE;IAChB,MAAM,IAAIK,KAAK,CACb,6DACF,CAAC;EACH;EAEA,OAAO,IAAIK,oCAAiB,CAAC;IAC3BT,OAAO,EAAEM,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;IACpDT,WAAW;IACXI,UAAU;IACVD;EACF,CAAC,CAAC;AACJ;AAAC,IAAAS,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAEc;EACblB,6BAA6B;EAC7Be;AACF,CAAC","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckWorker.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckWorker.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"healthCheckWorker.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckWorker.js"],"names":[],"mappings":"AAkBA,2EAuDC"}
|
|
@@ -8,52 +8,62 @@ exports.createHealthCheckWorker = createHealthCheckWorker;
|
|
|
8
8
|
* Shared health check worker utility.
|
|
9
9
|
* This runs as a separate process and periodically refreshes the health check cache,
|
|
10
10
|
* preventing HTTP requests from triggering database queries.
|
|
11
|
-
*
|
|
11
|
+
* Uses Redis client from service config (should be created with name 'healthcheck-worker').
|
|
12
12
|
*
|
|
13
13
|
* @param {Object} options
|
|
14
14
|
* @param {string} options.databaseUrl - Main database URL
|
|
15
15
|
* @param {string} options.databaseName - Database name
|
|
16
|
+
* @param {any} options.redisClient - Redis client instance (from service config with name 'healthcheck-worker')
|
|
16
17
|
* @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
|
|
17
18
|
* @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
|
|
18
|
-
* @param {
|
|
19
|
+
* @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response
|
|
19
20
|
* @param {number} [options.refreshIntervalMs=60000] - Refresh interval in milliseconds (default: 1 minute)
|
|
20
21
|
* @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
|
|
21
22
|
*/
|
|
22
23
|
const {
|
|
23
24
|
createHealthCheckWorkerClient
|
|
24
|
-
} = require('./healthCheckUtils');
|
|
25
|
+
} = require('./healthCheckUtils').default;
|
|
25
26
|
function createHealthCheckWorker(options) {
|
|
26
27
|
const {
|
|
27
28
|
refreshIntervalMs = 60000,
|
|
28
29
|
...workerClientOptions
|
|
29
30
|
} = options;
|
|
31
|
+
const appName = workerClientOptions.appName || process.env.BUILD_APP_NAME || 'unknown-app';
|
|
32
|
+
const dynoId = process.env.HOSTNAME || 'unknown-dyno';
|
|
33
|
+
const processType = process.env.BUILD_DYNO_PROCESS_TYPE || 'health-check-worker';
|
|
34
|
+
const logValues = process.env.HEALTH_LOG_VALUES === 'true';
|
|
35
|
+
const prefixLogs = `[${processType}] [${appName}] [${dynoId}] [HealthCheck]`;
|
|
30
36
|
const healthCheckClient = createHealthCheckWorkerClient(workerClientOptions);
|
|
31
37
|
return async function runHealthCheckWorker() {
|
|
32
|
-
console.log(
|
|
33
|
-
console.log(
|
|
38
|
+
console.log(`${prefixLogs} Starting health check worker...`);
|
|
39
|
+
console.log(`${prefixLogs} Refresh interval: ${refreshIntervalMs}ms`);
|
|
34
40
|
try {
|
|
35
41
|
await healthCheckClient.refreshCache();
|
|
36
|
-
|
|
42
|
+
if (logValues) {
|
|
43
|
+
console.log(`${prefixLogs} Initial health check completed`);
|
|
44
|
+
}
|
|
37
45
|
} catch (err) {
|
|
38
|
-
console.error(
|
|
46
|
+
console.error(`${prefixLogs} Initial health check failed:`, err);
|
|
39
47
|
}
|
|
40
48
|
const interval = setInterval(async () => {
|
|
41
49
|
try {
|
|
42
50
|
const result = await healthCheckClient.refreshCache();
|
|
43
|
-
|
|
51
|
+
if (logValues) {
|
|
52
|
+
console.log(`${prefixLogs} Health check refreshed at ${result.timestamp}, status: ${result.status}`);
|
|
53
|
+
}
|
|
44
54
|
} catch (err) {
|
|
45
|
-
console.error(
|
|
55
|
+
console.error(`${prefixLogs} Health check refresh failed:`, err);
|
|
46
56
|
}
|
|
47
57
|
}, refreshIntervalMs);
|
|
48
58
|
process.on('SIGTERM', () => {
|
|
49
|
-
console.log(
|
|
59
|
+
console.log(`${prefixLogs} Received SIGTERM, shutting down...`);
|
|
50
60
|
clearInterval(interval);
|
|
51
61
|
healthCheckClient.cleanup().finally(() => {
|
|
52
62
|
process.exit(0);
|
|
53
63
|
});
|
|
54
64
|
});
|
|
55
65
|
process.on('SIGINT', () => {
|
|
56
|
-
console.log(
|
|
66
|
+
console.log(`${prefixLogs} Received SIGINT, shutting down...`);
|
|
57
67
|
clearInterval(interval);
|
|
58
68
|
healthCheckClient.cleanup().finally(() => {
|
|
59
69
|
process.exit(0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckWorker.js","names":["createHealthCheckWorkerClient","require","createHealthCheckWorker","options","refreshIntervalMs","workerClientOptions","healthCheckClient","runHealthCheckWorker","console","log","refreshCache","err","error","interval","setInterval","result","timestamp","status","
|
|
1
|
+
{"version":3,"file":"healthCheckWorker.js","names":["createHealthCheckWorkerClient","require","default","createHealthCheckWorker","options","refreshIntervalMs","workerClientOptions","appName","process","env","BUILD_APP_NAME","dynoId","HOSTNAME","processType","BUILD_DYNO_PROCESS_TYPE","logValues","HEALTH_LOG_VALUES","prefixLogs","healthCheckClient","runHealthCheckWorker","console","log","refreshCache","err","error","interval","setInterval","result","timestamp","status","on","clearInterval","cleanup","finally","exit"],"sources":["../../src/health/healthCheckWorker.js"],"sourcesContent":["/**\n * Shared health check worker utility.\n * This runs as a separate process and periodically refreshes the health check cache,\n * preventing HTTP requests from triggering database queries.\n * Uses Redis client from service config (should be created with name 'healthcheck-worker').\n *\n * @param {Object} options\n * @param {string} options.databaseUrl - Main database URL\n * @param {string} options.databaseName - Database name\n * @param {any} options.redisClient - Redis client instance (from service config with name 'healthcheck-worker')\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 {boolean} [options.includeRedisCheck=false] - Include Redis health check in response\n * @param {number} [options.refreshIntervalMs=60000] - Refresh interval in milliseconds (default: 1 minute)\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds\n */\nconst { createHealthCheckWorkerClient } = require('./healthCheckUtils').default\n\nexport function createHealthCheckWorker(options) {\n const { refreshIntervalMs = 60000, ...workerClientOptions } = options\n\n const appName =\n workerClientOptions.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n const dynoId = process.env.HOSTNAME || 'unknown-dyno'\n const processType =\n process.env.BUILD_DYNO_PROCESS_TYPE || 'health-check-worker'\n const logValues = process.env.HEALTH_LOG_VALUES === 'true'\n const prefixLogs = `[${processType}] [${appName}] [${dynoId}] [HealthCheck]`\n\n const healthCheckClient = createHealthCheckWorkerClient(workerClientOptions)\n\n return async function runHealthCheckWorker() {\n console.log(`${prefixLogs} Starting health check worker...`)\n console.log(`${prefixLogs} Refresh interval: ${refreshIntervalMs}ms`)\n\n try {\n await healthCheckClient.refreshCache()\n if (logValues) {\n console.log(`${prefixLogs} Initial health check completed`)\n }\n } catch (err) {\n console.error(`${prefixLogs} Initial health check failed:`, err)\n }\n\n const interval = setInterval(async () => {\n try {\n const result = await healthCheckClient.refreshCache()\n if (logValues) {\n console.log(\n `${prefixLogs} Health check refreshed at ${result.timestamp}, status: ${result.status}`\n )\n }\n } catch (err) {\n console.error(`${prefixLogs} Health check refresh failed:`, err)\n }\n }, refreshIntervalMs)\n\n process.on('SIGTERM', () => {\n console.log(`${prefixLogs} Received SIGTERM, shutting down...`)\n clearInterval(interval)\n healthCheckClient.cleanup().finally(() => {\n process.exit(0)\n })\n })\n\n process.on('SIGINT', () => {\n console.log(`${prefixLogs} Received SIGINT, shutting down...`)\n clearInterval(interval)\n healthCheckClient.cleanup().finally(() => {\n process.exit(0)\n })\n })\n }\n}\n"],"mappings":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;EAAEA;AAA8B,CAAC,GAAGC,OAAO,CAAC,oBAAoB,CAAC,CAACC,OAAO;AAExE,SAASC,uBAAuBA,CAACC,OAAO,EAAE;EAC/C,MAAM;IAAEC,iBAAiB,GAAG,KAAK;IAAE,GAAGC;EAAoB,CAAC,GAAGF,OAAO;EAErE,MAAMG,OAAO,GACXD,mBAAmB,CAACC,OAAO,IAAIC,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;EAC5E,MAAMC,MAAM,GAAGH,OAAO,CAACC,GAAG,CAACG,QAAQ,IAAI,cAAc;EACrD,MAAMC,WAAW,GACfL,OAAO,CAACC,GAAG,CAACK,uBAAuB,IAAI,qBAAqB;EAC9D,MAAMC,SAAS,GAAGP,OAAO,CAACC,GAAG,CAACO,iBAAiB,KAAK,MAAM;EAC1D,MAAMC,UAAU,GAAG,IAAIJ,WAAW,MAAMN,OAAO,MAAMI,MAAM,iBAAiB;EAE5E,MAAMO,iBAAiB,GAAGlB,6BAA6B,CAACM,mBAAmB,CAAC;EAE5E,OAAO,eAAea,oBAAoBA,CAAA,EAAG;IAC3CC,OAAO,CAACC,GAAG,CAAC,GAAGJ,UAAU,kCAAkC,CAAC;IAC5DG,OAAO,CAACC,GAAG,CAAC,GAAGJ,UAAU,sBAAsBZ,iBAAiB,IAAI,CAAC;IAErE,IAAI;MACF,MAAMa,iBAAiB,CAACI,YAAY,CAAC,CAAC;MACtC,IAAIP,SAAS,EAAE;QACbK,OAAO,CAACC,GAAG,CAAC,GAAGJ,UAAU,iCAAiC,CAAC;MAC7D;IACF,CAAC,CAAC,OAAOM,GAAG,EAAE;MACZH,OAAO,CAACI,KAAK,CAAC,GAAGP,UAAU,+BAA+B,EAAEM,GAAG,CAAC;IAClE;IAEA,MAAME,QAAQ,GAAGC,WAAW,CAAC,YAAY;MACvC,IAAI;QACF,MAAMC,MAAM,GAAG,MAAMT,iBAAiB,CAACI,YAAY,CAAC,CAAC;QACrD,IAAIP,SAAS,EAAE;UACbK,OAAO,CAACC,GAAG,CACT,GAAGJ,UAAU,8BAA8BU,MAAM,CAACC,SAAS,aAAaD,MAAM,CAACE,MAAM,EACvF,CAAC;QACH;MACF,CAAC,CAAC,OAAON,GAAG,EAAE;QACZH,OAAO,CAACI,KAAK,CAAC,GAAGP,UAAU,+BAA+B,EAAEM,GAAG,CAAC;MAClE;IACF,CAAC,EAAElB,iBAAiB,CAAC;IAErBG,OAAO,CAACsB,EAAE,CAAC,SAAS,EAAE,MAAM;MAC1BV,OAAO,CAACC,GAAG,CAAC,GAAGJ,UAAU,qCAAqC,CAAC;MAC/Dc,aAAa,CAACN,QAAQ,CAAC;MACvBP,iBAAiB,CAACc,OAAO,CAAC,CAAC,CAACC,OAAO,CAAC,MAAM;QACxCzB,OAAO,CAAC0B,IAAI,CAAC,CAAC,CAAC;MACjB,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF1B,OAAO,CAACsB,EAAE,CAAC,QAAQ,EAAE,MAAM;MACzBV,OAAO,CAACC,GAAG,CAAC,GAAGJ,UAAU,oCAAoC,CAAC;MAC9Dc,aAAa,CAACN,QAAQ,CAAC;MACvBP,iBAAiB,CAACc,OAAO,CAAC,CAAC,CAACC,OAAO,CAAC,MAAM;QACxCzB,OAAO,CAAC0B,IAAI,CAAC,CAAC,CAAC;MACjB,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ,CAAC;AACH","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -19,10 +19,11 @@ class HealthCheckCache {
|
|
|
19
19
|
*/
|
|
20
20
|
constructor(options = {}) {
|
|
21
21
|
this.redisClient = options.redisClient || null
|
|
22
|
-
this.appName =
|
|
22
|
+
this.appName =
|
|
23
|
+
options.appName || process.env.BUILD_APP_NAME || 'unknown-app'
|
|
23
24
|
this.cacheTtlMs = options.cacheTtlMs ?? 60 * 1000
|
|
24
25
|
this.cacheKey = `healthcheck:${this.appName}`
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
/** In-memory fallback cache */
|
|
27
28
|
this._memoryCache = null
|
|
28
29
|
this._memoryCacheTimestamp = null
|
|
@@ -165,15 +166,10 @@ class HealthCheckCache {
|
|
|
165
166
|
|
|
166
167
|
if (this._redisClientType === REDIS_V3) {
|
|
167
168
|
await new Promise((resolve, reject) => {
|
|
168
|
-
this.redisClient.setex(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
(err) => {
|
|
173
|
-
if (err) reject(err)
|
|
174
|
-
else resolve()
|
|
175
|
-
}
|
|
176
|
-
)
|
|
169
|
+
this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr, err => {
|
|
170
|
+
if (err) reject(err)
|
|
171
|
+
else resolve()
|
|
172
|
+
})
|
|
177
173
|
})
|
|
178
174
|
} else if (
|
|
179
175
|
this._redisClientType === REDIS_V4 ||
|
|
@@ -204,7 +200,7 @@ class HealthCheckCache {
|
|
|
204
200
|
try {
|
|
205
201
|
if (this._redisClientType === REDIS_V3) {
|
|
206
202
|
await new Promise((resolve, reject) => {
|
|
207
|
-
this.redisClient.del(this.cacheKey,
|
|
203
|
+
this.redisClient.del(this.cacheKey, err => {
|
|
208
204
|
if (err) reject(err)
|
|
209
205
|
else resolve()
|
|
210
206
|
})
|
|
@@ -216,10 +212,7 @@ class HealthCheckCache {
|
|
|
216
212
|
await this.redisClient.del(this.cacheKey)
|
|
217
213
|
}
|
|
218
214
|
} catch (redisErr) {
|
|
219
|
-
console.warn(
|
|
220
|
-
`[HealthCheckCache] Redis clear failed:`,
|
|
221
|
-
redisErr.message
|
|
222
|
-
)
|
|
215
|
+
console.warn(`[HealthCheckCache] Redis clear failed:`, redisErr.message)
|
|
223
216
|
}
|
|
224
217
|
}
|
|
225
218
|
}
|
|
@@ -234,4 +227,3 @@ class HealthCheckCache {
|
|
|
234
227
|
}
|
|
235
228
|
|
|
236
229
|
module.exports = { HealthCheckCache }
|
|
237
|
-
|
|
@@ -15,7 +15,8 @@ const HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000
|
|
|
15
15
|
const SENSITIVE_PATTERNS = [
|
|
16
16
|
// Database connection strings: postgres://user:password@host:port/database
|
|
17
17
|
{
|
|
18
|
-
pattern:
|
|
18
|
+
pattern:
|
|
19
|
+
/(postgres(?:ql)?|mysql|mongodb|redis|amqp):\/\/([^:]+):([^@]+)@([^:/]+)(:\d+)?\/([^\s?]+)/gi,
|
|
19
20
|
replacement: '$1://***:***@***$5/***',
|
|
20
21
|
},
|
|
21
22
|
// Generic URLs with credentials: protocol://user:password@host
|
|
@@ -25,12 +26,14 @@ const SENSITIVE_PATTERNS = [
|
|
|
25
26
|
},
|
|
26
27
|
// Password fields in JSON or key=value format
|
|
27
28
|
{
|
|
28
|
-
pattern:
|
|
29
|
+
pattern:
|
|
30
|
+
/(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
29
31
|
replacement: '$1=***',
|
|
30
32
|
},
|
|
31
33
|
// Database/table/schema/role/user names in error messages: database "name", table "name", etc.
|
|
32
34
|
{
|
|
33
|
-
pattern:
|
|
35
|
+
pattern:
|
|
36
|
+
/(database|table|schema|role|user|relation|column|index)\s*["']([^"']+)["']/gi,
|
|
34
37
|
replacement: '$1 "***"',
|
|
35
38
|
},
|
|
36
39
|
// IP addresses (to hide internal network structure)
|
|
@@ -520,7 +523,8 @@ class HealthCheckClient {
|
|
|
520
523
|
if (db.clusters) {
|
|
521
524
|
for (const [name, health] of Object.entries(db.clusters)) {
|
|
522
525
|
if (health.status === 'unhealthy') {
|
|
523
|
-
const message =
|
|
526
|
+
const message =
|
|
527
|
+
health.error || health.message || 'connection failed'
|
|
524
528
|
errors.push(`DB ${name}: ${maskSensitiveData(message)}`)
|
|
525
529
|
}
|
|
526
530
|
}
|
|
@@ -528,7 +532,10 @@ class HealthCheckClient {
|
|
|
528
532
|
}
|
|
529
533
|
|
|
530
534
|
if (result.checks.redis && result.checks.redis.status === 'unhealthy') {
|
|
531
|
-
const message =
|
|
535
|
+
const message =
|
|
536
|
+
result.checks.redis.error ||
|
|
537
|
+
result.checks.redis.message ||
|
|
538
|
+
'connection failed'
|
|
532
539
|
errors.push(`Redis: ${maskSensitiveData(message)}`)
|
|
533
540
|
}
|
|
534
541
|
|
|
@@ -554,7 +561,8 @@ class HealthCheckClient {
|
|
|
554
561
|
|
|
555
562
|
if (!result) {
|
|
556
563
|
// No cache available - return unhealthy status with proper format
|
|
557
|
-
const errorMsg =
|
|
564
|
+
const errorMsg =
|
|
565
|
+
'Health check cache not available. Worker may not be running.'
|
|
558
566
|
res.status(503).json({
|
|
559
567
|
status: 'unhealthy',
|
|
560
568
|
timestamp: new Date().toISOString(),
|
|
@@ -573,7 +581,7 @@ class HealthCheckClient {
|
|
|
573
581
|
|
|
574
582
|
// Build response - errors are already in result if present
|
|
575
583
|
const response = { ...result }
|
|
576
|
-
|
|
584
|
+
|
|
577
585
|
// Add top-level errors array if not healthy and errors not already present
|
|
578
586
|
if (result.status !== 'healthy' && !result.errors) {
|
|
579
587
|
const errors = this._getErrorMessages(result)
|
|
@@ -621,7 +629,10 @@ class HealthCheckClient {
|
|
|
621
629
|
try {
|
|
622
630
|
await pool.end()
|
|
623
631
|
} catch (err) {
|
|
624
|
-
console.error(
|
|
632
|
+
console.error(
|
|
633
|
+
`${this.prefixLogs} Error closing database pool ${name}:`,
|
|
634
|
+
err
|
|
635
|
+
)
|
|
625
636
|
}
|
|
626
637
|
}
|
|
627
638
|
this._databasePools.clear()
|
|
@@ -1,66 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
import { HealthCheckClient } from './healthCheckClient'
|
|
64
2
|
|
|
65
3
|
/**
|
|
66
4
|
* Creates a health check client for worker process.
|
|
@@ -69,29 +7,27 @@ function createHealthCheckRedisClient(options = {}) {
|
|
|
69
7
|
* @param {Object} options
|
|
70
8
|
* @param {string} options.databaseUrl - Main database URL
|
|
71
9
|
* @param {string} options.databaseName - Database name
|
|
10
|
+
* @param {any} options.redisClient - Redis client instance (from service config with appropriate name)
|
|
72
11
|
* @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
|
|
73
12
|
* @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
|
|
74
|
-
* @param {
|
|
13
|
+
* @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response
|
|
75
14
|
* @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
|
|
76
15
|
* @returns {HealthCheckClient} Configured health check client instance for worker
|
|
77
16
|
*/
|
|
78
17
|
function createHealthCheckWorkerClient(options) {
|
|
79
|
-
const { HealthCheckClient } = require('./healthCheckClient')
|
|
80
18
|
const {
|
|
81
19
|
databaseUrl,
|
|
82
20
|
databaseName,
|
|
21
|
+
redisClient,
|
|
83
22
|
appName,
|
|
84
23
|
additionalDatabaseUrls,
|
|
85
|
-
redisUrl,
|
|
86
24
|
includeRedisCheck = false,
|
|
87
25
|
cacheTtlMs = 60000,
|
|
88
26
|
} = options
|
|
89
27
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
clientName: 'healthcheck-worker',
|
|
94
|
-
})
|
|
28
|
+
if (!redisClient) {
|
|
29
|
+
throw new Error('redisClient is required for createHealthCheckWorkerClient')
|
|
30
|
+
}
|
|
95
31
|
|
|
96
32
|
const clientOptions = {
|
|
97
33
|
databaseUrl,
|
|
@@ -120,11 +56,12 @@ function createHealthCheckWorkerClient(options) {
|
|
|
120
56
|
* @returns {HealthCheckClient} Configured health check client instance for endpoints
|
|
121
57
|
*/
|
|
122
58
|
function createHealthCheckEndpointClient(options) {
|
|
123
|
-
const { HealthCheckClient } = require('./healthCheckClient')
|
|
124
59
|
const { redisClient, includeRedisCheck = false, cacheTtlMs = 60000 } = options
|
|
125
60
|
|
|
126
61
|
if (!redisClient) {
|
|
127
|
-
throw new Error(
|
|
62
|
+
throw new Error(
|
|
63
|
+
'redisClient is required for createHealthCheckEndpointClient'
|
|
64
|
+
)
|
|
128
65
|
}
|
|
129
66
|
|
|
130
67
|
return new HealthCheckClient({
|
|
@@ -135,9 +72,7 @@ function createHealthCheckEndpointClient(options) {
|
|
|
135
72
|
})
|
|
136
73
|
}
|
|
137
74
|
|
|
138
|
-
|
|
139
|
-
createHealthCheckRedisClient,
|
|
75
|
+
export default {
|
|
140
76
|
createHealthCheckWorkerClient,
|
|
141
77
|
createHealthCheckEndpointClient,
|
|
142
78
|
}
|
|
143
|
-
|
|
@@ -2,48 +2,61 @@
|
|
|
2
2
|
* Shared health check worker utility.
|
|
3
3
|
* This runs as a separate process and periodically refreshes the health check cache,
|
|
4
4
|
* preventing HTTP requests from triggering database queries.
|
|
5
|
-
*
|
|
5
|
+
* Uses Redis client from service config (should be created with name 'healthcheck-worker').
|
|
6
6
|
*
|
|
7
7
|
* @param {Object} options
|
|
8
8
|
* @param {string} options.databaseUrl - Main database URL
|
|
9
9
|
* @param {string} options.databaseName - Database name
|
|
10
|
+
* @param {any} options.redisClient - Redis client instance (from service config with name 'healthcheck-worker')
|
|
10
11
|
* @param {string} [options.appName] - Application name (defaults to BUILD_APP_NAME)
|
|
11
12
|
* @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional database clusters (for database service)
|
|
12
|
-
* @param {
|
|
13
|
+
* @param {boolean} [options.includeRedisCheck=false] - Include Redis health check in response
|
|
13
14
|
* @param {number} [options.refreshIntervalMs=60000] - Refresh interval in milliseconds (default: 1 minute)
|
|
14
15
|
* @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds
|
|
15
16
|
*/
|
|
16
|
-
const { createHealthCheckWorkerClient } = require('./healthCheckUtils')
|
|
17
|
+
const { createHealthCheckWorkerClient } = require('./healthCheckUtils').default
|
|
17
18
|
|
|
18
19
|
export function createHealthCheckWorker(options) {
|
|
19
20
|
const { refreshIntervalMs = 60000, ...workerClientOptions } = options
|
|
20
21
|
|
|
22
|
+
const appName =
|
|
23
|
+
workerClientOptions.appName || process.env.BUILD_APP_NAME || 'unknown-app'
|
|
24
|
+
const dynoId = process.env.HOSTNAME || 'unknown-dyno'
|
|
25
|
+
const processType =
|
|
26
|
+
process.env.BUILD_DYNO_PROCESS_TYPE || 'health-check-worker'
|
|
27
|
+
const logValues = process.env.HEALTH_LOG_VALUES === 'true'
|
|
28
|
+
const prefixLogs = `[${processType}] [${appName}] [${dynoId}] [HealthCheck]`
|
|
29
|
+
|
|
21
30
|
const healthCheckClient = createHealthCheckWorkerClient(workerClientOptions)
|
|
22
31
|
|
|
23
32
|
return async function runHealthCheckWorker() {
|
|
24
|
-
console.log(
|
|
25
|
-
console.log(
|
|
33
|
+
console.log(`${prefixLogs} Starting health check worker...`)
|
|
34
|
+
console.log(`${prefixLogs} Refresh interval: ${refreshIntervalMs}ms`)
|
|
26
35
|
|
|
27
36
|
try {
|
|
28
37
|
await healthCheckClient.refreshCache()
|
|
29
|
-
|
|
38
|
+
if (logValues) {
|
|
39
|
+
console.log(`${prefixLogs} Initial health check completed`)
|
|
40
|
+
}
|
|
30
41
|
} catch (err) {
|
|
31
|
-
console.error(
|
|
42
|
+
console.error(`${prefixLogs} Initial health check failed:`, err)
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
const interval = setInterval(async () => {
|
|
35
46
|
try {
|
|
36
47
|
const result = await healthCheckClient.refreshCache()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
if (logValues) {
|
|
49
|
+
console.log(
|
|
50
|
+
`${prefixLogs} Health check refreshed at ${result.timestamp}, status: ${result.status}`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
40
53
|
} catch (err) {
|
|
41
|
-
console.error(
|
|
54
|
+
console.error(`${prefixLogs} Health check refresh failed:`, err)
|
|
42
55
|
}
|
|
43
56
|
}, refreshIntervalMs)
|
|
44
57
|
|
|
45
58
|
process.on('SIGTERM', () => {
|
|
46
|
-
console.log(
|
|
59
|
+
console.log(`${prefixLogs} Received SIGTERM, shutting down...`)
|
|
47
60
|
clearInterval(interval)
|
|
48
61
|
healthCheckClient.cleanup().finally(() => {
|
|
49
62
|
process.exit(0)
|
|
@@ -51,7 +64,7 @@ export function createHealthCheckWorker(options) {
|
|
|
51
64
|
})
|
|
52
65
|
|
|
53
66
|
process.on('SIGINT', () => {
|
|
54
|
-
console.log(
|
|
67
|
+
console.log(`${prefixLogs} Received SIGINT, shutting down...`)
|
|
55
68
|
clearInterval(interval)
|
|
56
69
|
healthCheckClient.cleanup().finally(() => {
|
|
57
70
|
process.exit(0)
|