@adalo/metrics 0.0.0-staging.1
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/.env.example +14 -0
- package/.eslintignore +3 -0
- package/.eslintrc +61 -0
- package/.github/pull_request_template.md +14 -0
- package/.github/workflows/code-style.yml +29 -0
- package/.github/workflows/deploy-staging.yml +34 -0
- package/.github/workflows/deploy.yml +29 -0
- package/.github/workflows/tests.yml +17 -0
- package/.idea/codeStyles/Project.xml +101 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/git_toolbox_prj.xml +15 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierrc +10 -0
- package/README-health.md +234 -0
- package/README.md +120 -0
- package/__tests__/metricsRedisClient.test.js +138 -0
- package/babel.config.js +20 -0
- package/lib/health/databaseChecker.d.ts +43 -0
- package/lib/health/databaseChecker.d.ts.map +1 -0
- package/lib/health/databaseChecker.js +189 -0
- package/lib/health/databaseChecker.js.map +1 -0
- package/lib/health/healthCheckCache.d.ts +59 -0
- package/lib/health/healthCheckCache.d.ts.map +1 -0
- package/lib/health/healthCheckCache.js +187 -0
- package/lib/health/healthCheckCache.js.map +1 -0
- package/lib/health/healthCheckClient.d.ts +124 -0
- package/lib/health/healthCheckClient.d.ts.map +1 -0
- package/lib/health/healthCheckClient.js +324 -0
- package/lib/health/healthCheckClient.js.map +1 -0
- package/lib/health/healthCheckUtils.d.ts +52 -0
- package/lib/health/healthCheckUtils.d.ts.map +1 -0
- package/lib/health/healthCheckUtils.js +129 -0
- package/lib/health/healthCheckUtils.js.map +1 -0
- package/lib/health/healthCheckWorker.d.ts +2 -0
- package/lib/health/healthCheckWorker.d.ts.map +1 -0
- package/lib/health/healthCheckWorker.js +70 -0
- package/lib/health/healthCheckWorker.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +105 -0
- package/lib/index.js.map +1 -0
- package/lib/metrics/baseMetricsClient.d.ts +174 -0
- package/lib/metrics/baseMetricsClient.d.ts.map +1 -0
- package/lib/metrics/baseMetricsClient.js +428 -0
- package/lib/metrics/baseMetricsClient.js.map +1 -0
- package/lib/metrics/metricsClient.d.ts +95 -0
- package/lib/metrics/metricsClient.d.ts.map +1 -0
- package/lib/metrics/metricsClient.js +239 -0
- package/lib/metrics/metricsClient.js.map +1 -0
- package/lib/metrics/metricsDatabaseClient.d.ts +74 -0
- package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -0
- package/lib/metrics/metricsDatabaseClient.js +218 -0
- package/lib/metrics/metricsDatabaseClient.js.map +1 -0
- package/lib/metrics/metricsQueueRedisClient.d.ts +57 -0
- package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -0
- package/lib/metrics/metricsQueueRedisClient.js +277 -0
- package/lib/metrics/metricsQueueRedisClient.js.map +1 -0
- package/lib/metrics/metricsRedisClient.d.ts +71 -0
- package/lib/metrics/metricsRedisClient.d.ts.map +1 -0
- package/lib/metrics/metricsRedisClient.js +370 -0
- package/lib/metrics/metricsRedisClient.js.map +1 -0
- package/lib/redisUtils.d.ts +53 -0
- package/lib/redisUtils.d.ts.map +1 -0
- package/lib/redisUtils.js +140 -0
- package/lib/redisUtils.js.map +1 -0
- package/package.json +66 -0
- package/scripts/README.md +43 -0
- package/scripts/clearMetrics.js +6 -0
- package/src/health/databaseChecker.js +183 -0
- package/src/health/healthCheckCache.js +216 -0
- package/src/health/healthCheckClient.js +347 -0
- package/src/health/healthCheckUtils.js +125 -0
- package/src/health/healthCheckWorker.js +71 -0
- package/src/index.ts +9 -0
- package/src/metrics/baseMetricsClient.js +494 -0
- package/src/metrics/metricsClient.js +284 -0
- package/src/metrics/metricsDatabaseClient.js +236 -0
- package/src/metrics/metricsQueueRedisClient.js +352 -0
- package/src/metrics/metricsRedisClient.js +417 -0
- package/src/redisUtils.js +155 -0
- package/tsconfig.json +19 -0
- package/tsconfig.types.json +11 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createDatabasePool,
|
|
3
|
+
runHealthCheck,
|
|
4
|
+
closePool,
|
|
5
|
+
} = require('./databaseChecker')
|
|
6
|
+
const {
|
|
7
|
+
getRedisClientType,
|
|
8
|
+
REDIS_V4,
|
|
9
|
+
IOREDIS,
|
|
10
|
+
REDIS_V3,
|
|
11
|
+
} = require('../redisUtils')
|
|
12
|
+
const { HealthCheckCache } = require('./healthCheckCache')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} name
|
|
16
|
+
* @returns {number | undefined}
|
|
17
|
+
*/
|
|
18
|
+
function readNumberEnv(name) {
|
|
19
|
+
const raw = process.env[name]
|
|
20
|
+
if (raw == null || raw === '') return undefined
|
|
21
|
+
const num = Number(raw)
|
|
22
|
+
return Number.isFinite(num) ? num : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number, maxDbConnectLatencyMs: number }} */
|
|
26
|
+
const DEFAULT_HEALTH_CONFIG = {
|
|
27
|
+
checkIntervalMs: 30_000,
|
|
28
|
+
staleThresholdMs: 180_000,
|
|
29
|
+
checkTimeoutMs: 15_000,
|
|
30
|
+
maxDbConnectLatencyMs:
|
|
31
|
+
readNumberEnv('HEALTH_DB_MAX_CONNECT_LATENCY_MS') ?? 1000,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SENSITIVE_PATTERNS = [
|
|
35
|
+
{
|
|
36
|
+
pattern:
|
|
37
|
+
/(postgres(?:ql)?|mysql|mongodb|redis|amqp):\/\/([^:]+):([^@]+)@([^:/]+)(:\d+)?\/([^\s?]+)/gi,
|
|
38
|
+
replacement: '$1://***:***@***$5/***',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: /(\w+):\/\/([^:]+):([^@]+)@([^\s/]+)/gi,
|
|
42
|
+
replacement: '$1://***:***@***',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
pattern:
|
|
46
|
+
/(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
47
|
+
replacement: '$1=***',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
pattern:
|
|
51
|
+
/(database|table|schema|role|user|relation|column|index)\s*["']([^"']+)["']/gi,
|
|
52
|
+
replacement: '$1 "***"',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?\b/g,
|
|
56
|
+
replacement: '***$2',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
pattern: /\b(host|hostname|server)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
60
|
+
replacement: '$1=***',
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} text
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function maskSensitiveData(text) {
|
|
69
|
+
if (!text || typeof text !== 'string') return text
|
|
70
|
+
let masked = text
|
|
71
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
72
|
+
masked = masked.replace(pattern, replacement)
|
|
73
|
+
}
|
|
74
|
+
return masked
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {{ env: string, url?: string } | { env: string, client?: any }} HealthResource
|
|
79
|
+
*/
|
|
80
|
+
class HealthCheckClient {
|
|
81
|
+
/**
|
|
82
|
+
* @param {Object} options
|
|
83
|
+
* @param {HealthResource[]} options.resources - Must include Redis resource with client
|
|
84
|
+
* @param {Object} [options.config]
|
|
85
|
+
* @param {string} [options.appName] - For cache key: healthcheck:${appName}
|
|
86
|
+
* @param {string} [options.cacheKey] - Redis key (overrides appName)
|
|
87
|
+
*/
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
this.healthConfig = { ...DEFAULT_HEALTH_CONFIG, ...options.config }
|
|
90
|
+
this.appName =
|
|
91
|
+
options.appName || process.env.BUILD_APP_NAME || 'unknown-app'
|
|
92
|
+
|
|
93
|
+
this.prefixLogs = `[${this.appName}] [HealthCheck]`
|
|
94
|
+
|
|
95
|
+
this._refreshPromise = null
|
|
96
|
+
/** @type {Map<string, { pool: any, type: string }>} */
|
|
97
|
+
this._databasePools = new Map()
|
|
98
|
+
|
|
99
|
+
/** @type {HealthResource[]} */
|
|
100
|
+
this._resources = options.resources || []
|
|
101
|
+
|
|
102
|
+
const redisClient = this._getRedisClientForCache()
|
|
103
|
+
if (redisClient) {
|
|
104
|
+
this._redisClientType = getRedisClientType(redisClient)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this._cache = new HealthCheckCache({
|
|
108
|
+
redisClient: redisClient || null,
|
|
109
|
+
cacheKey: options.cacheKey,
|
|
110
|
+
appName: this.appName,
|
|
111
|
+
staleThresholdMs: this.healthConfig.staleThresholdMs,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_getEnv(resource) {
|
|
116
|
+
return resource.env ?? resource.name
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_getRedisClientForCache() {
|
|
120
|
+
const redisResource = this._resources.find(r => 'client' in r && r.client)
|
|
121
|
+
return redisResource?.client || null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_getPool(env, url) {
|
|
125
|
+
if (!this._databasePools.has(env)) {
|
|
126
|
+
const { pool, type } = createDatabasePool(
|
|
127
|
+
env,
|
|
128
|
+
url,
|
|
129
|
+
this.healthConfig.checkTimeoutMs
|
|
130
|
+
)
|
|
131
|
+
this._databasePools.set(env, { pool, type })
|
|
132
|
+
}
|
|
133
|
+
return this._databasePools.get(env)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async _checkDatabase(resource) {
|
|
137
|
+
const env = this._getEnv(resource)
|
|
138
|
+
const url = 'url' in resource ? resource.url : process.env[env]
|
|
139
|
+
if (!url) {
|
|
140
|
+
return { status: 'error', error: `Env ${env} not set` }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const { pool, type } = this._getPool(env, url)
|
|
145
|
+
const timings = await runHealthCheck(pool, type)
|
|
146
|
+
|
|
147
|
+
const maxConnect = this.healthConfig.maxDbConnectLatencyMs
|
|
148
|
+
if (
|
|
149
|
+
typeof maxConnect === 'number' &&
|
|
150
|
+
Number.isFinite(maxConnect) &&
|
|
151
|
+
timings?.connectMs != null &&
|
|
152
|
+
timings.connectMs > maxConnect
|
|
153
|
+
) {
|
|
154
|
+
return {
|
|
155
|
+
status: 'error',
|
|
156
|
+
error: `DB connect latency ${timings.connectMs}ms exceeds ${maxConnect}ms`,
|
|
157
|
+
connectMs: timings.connectMs,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { status: 'ok', connectMs: timings.connectMs }
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const timings = err?.healthCheckTimings
|
|
164
|
+
return {
|
|
165
|
+
status: 'error',
|
|
166
|
+
error: maskSensitiveData(err.message),
|
|
167
|
+
...(timings && typeof timings === 'object'
|
|
168
|
+
? { connectMs: timings.connectMs }
|
|
169
|
+
: {}),
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async _checkRedis(resource) {
|
|
175
|
+
const { client } = resource
|
|
176
|
+
if (!client) return { status: 'ok' }
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
let pong
|
|
180
|
+
if (this._redisClientType === REDIS_V3) {
|
|
181
|
+
pong = await new Promise((resolve, reject) => {
|
|
182
|
+
client.ping((err, result) => {
|
|
183
|
+
if (err) reject(err)
|
|
184
|
+
else resolve(result)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
} else if (
|
|
188
|
+
this._redisClientType === REDIS_V4 ||
|
|
189
|
+
this._redisClientType === IOREDIS
|
|
190
|
+
) {
|
|
191
|
+
pong = await client.ping()
|
|
192
|
+
} else {
|
|
193
|
+
return { status: 'error', error: 'Unknown Redis client type' }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return pong === 'PONG'
|
|
197
|
+
? { status: 'ok' }
|
|
198
|
+
: { status: 'error', error: `Unexpected: ${pong}` }
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return { status: 'error', error: maskSensitiveData(err.message) }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async _performHealthCheckInternal() {
|
|
205
|
+
const resources = {}
|
|
206
|
+
|
|
207
|
+
for (const resource of this._resources) {
|
|
208
|
+
const env = this._getEnv(resource)
|
|
209
|
+
|
|
210
|
+
if ('client' in resource && resource.client) {
|
|
211
|
+
resources[env] = await this._checkRedis(resource)
|
|
212
|
+
} else {
|
|
213
|
+
resources[env] = await this._checkDatabase(resource)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const sortedResources = Object.keys(resources)
|
|
218
|
+
.sort()
|
|
219
|
+
.reduce((acc, key) => {
|
|
220
|
+
acc[key] = resources[key]
|
|
221
|
+
return acc
|
|
222
|
+
}, {})
|
|
223
|
+
|
|
224
|
+
const hasError = Object.values(resources).some(r => r.status === 'error')
|
|
225
|
+
const lastCheckAt = Date.now()
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
status: hasError ? 'error' : 'ok',
|
|
229
|
+
lastCheckAt,
|
|
230
|
+
resources: sortedResources,
|
|
231
|
+
isStale: false,
|
|
232
|
+
config: this.healthConfig,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_formatResult(result, cached = false) {
|
|
237
|
+
const isStale =
|
|
238
|
+
!result.lastCheckAt ||
|
|
239
|
+
Date.now() - result.lastCheckAt > this.healthConfig.staleThresholdMs
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
...result,
|
|
243
|
+
isStale,
|
|
244
|
+
status: isStale ? 'stale' : result.status,
|
|
245
|
+
...(isStale && {
|
|
246
|
+
error:
|
|
247
|
+
'Health check data is stale, health-check worker may not be running. Resource statuses are unknown.',
|
|
248
|
+
}),
|
|
249
|
+
...(cached && { cached: true }),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async performHealthCheck() {
|
|
254
|
+
if (this._refreshPromise) {
|
|
255
|
+
return this._refreshPromise
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this._refreshPromise = this._performHealthCheckInternal()
|
|
259
|
+
.then(result => {
|
|
260
|
+
this._refreshPromise = null
|
|
261
|
+
return this._formatResult(result)
|
|
262
|
+
})
|
|
263
|
+
.catch(err => {
|
|
264
|
+
this._refreshPromise = null
|
|
265
|
+
throw err
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
return this._refreshPromise
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async getCachedResult() {
|
|
272
|
+
try {
|
|
273
|
+
const cached = await this._cache.get()
|
|
274
|
+
if (cached) return this._formatResult(cached)
|
|
275
|
+
return null
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error(`${this.prefixLogs} Failed to read from cache:`, err)
|
|
278
|
+
return {
|
|
279
|
+
status: 'error',
|
|
280
|
+
lastCheckAt: null,
|
|
281
|
+
resources: {},
|
|
282
|
+
isStale: true,
|
|
283
|
+
error:
|
|
284
|
+
'Redis unavailable, unable to read health status of other resources',
|
|
285
|
+
config: this.healthConfig,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async refreshCache() {
|
|
291
|
+
const result = await this._performHealthCheckInternal()
|
|
292
|
+
await this._cache.set(result)
|
|
293
|
+
return result
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
clearCache() {
|
|
297
|
+
this._refreshPromise = null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
healthHandler() {
|
|
301
|
+
return async (req, res) => {
|
|
302
|
+
try {
|
|
303
|
+
const result = await this.getCachedResult()
|
|
304
|
+
|
|
305
|
+
if (!result) {
|
|
306
|
+
res.status(503).json({
|
|
307
|
+
status: 'error',
|
|
308
|
+
lastCheckAt: null,
|
|
309
|
+
resources: {},
|
|
310
|
+
isStale: true,
|
|
311
|
+
error:
|
|
312
|
+
'No health check data yet, health-check worker may not be running',
|
|
313
|
+
config: this.healthConfig,
|
|
314
|
+
})
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const statusCode = result.status === 'ok' ? 200 : 503
|
|
319
|
+
res.status(statusCode).json(result)
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.error(`${this.prefixLogs} Health check failed:`, err)
|
|
322
|
+
res.status(503).json({
|
|
323
|
+
status: 'error',
|
|
324
|
+
lastCheckAt: null,
|
|
325
|
+
resources: {},
|
|
326
|
+
isStale: true,
|
|
327
|
+
error:
|
|
328
|
+
'Redis unavailable, unable to read health status of other resources',
|
|
329
|
+
config: this.healthConfig,
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async cleanup() {
|
|
336
|
+
for (const [, { pool }] of this._databasePools) {
|
|
337
|
+
try {
|
|
338
|
+
await closePool(pool)
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.error(`${this.prefixLogs} Error closing database pool:`, err)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
this._databasePools.clear()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = { HealthCheckClient, DEFAULT_HEALTH_CONFIG }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const { HealthCheckClient } = require('./healthCheckClient')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object<string, string>} urls
|
|
5
|
+
* @returns {{ env: string, url: string }[]}
|
|
6
|
+
*/
|
|
7
|
+
function additionalDatabaseUrlsToResources(urls) {
|
|
8
|
+
if (!urls || typeof urls !== 'object') return []
|
|
9
|
+
return Object.entries(urls)
|
|
10
|
+
.filter(([, url]) => url)
|
|
11
|
+
.map(([env, url]) => ({ env, url }))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {{ env: string } | { env: string, url?: string } | { env: string, client?: any }}[] options.resources
|
|
17
|
+
* @param {string} [options.appName] - For cache key: healthcheck:${appName}
|
|
18
|
+
* @param {string} [options.cacheKey] - Redis key (overrides appName)
|
|
19
|
+
* @param {Object} [options.config]
|
|
20
|
+
* @param {string} [options.databaseUrl]
|
|
21
|
+
* @param {string} [options.databaseName]
|
|
22
|
+
* @param {Object<string, string>} [options.additionalDatabaseUrls]
|
|
23
|
+
* @returns {HealthCheckClient}
|
|
24
|
+
*/
|
|
25
|
+
function createHealthCheckWorkerClient(options) {
|
|
26
|
+
const {
|
|
27
|
+
resources,
|
|
28
|
+
appName,
|
|
29
|
+
cacheKey,
|
|
30
|
+
config,
|
|
31
|
+
databaseUrl,
|
|
32
|
+
databaseName,
|
|
33
|
+
additionalDatabaseUrls,
|
|
34
|
+
} = options
|
|
35
|
+
|
|
36
|
+
let resourcesArray = Array.isArray(resources) ? resources : []
|
|
37
|
+
|
|
38
|
+
if (resourcesArray.length === 0) {
|
|
39
|
+
resourcesArray = []
|
|
40
|
+
|
|
41
|
+
if (databaseUrl) {
|
|
42
|
+
const url = databaseUrl || process.env.DATABASE_URL
|
|
43
|
+
if (url) {
|
|
44
|
+
const env = databaseName || 'DATABASE_URL'
|
|
45
|
+
resourcesArray.push({ env, url })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const clusterResources = additionalDatabaseUrlsToResources(
|
|
50
|
+
additionalDatabaseUrls || {}
|
|
51
|
+
)
|
|
52
|
+
resourcesArray.push(...clusterResources)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const redisResource = resourcesArray.find(r => 'client' in r && r.client)
|
|
56
|
+
if (!redisResource) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'resources must include Redis resource with client (e.g. { env: "REDIS_URL", client })'
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new HealthCheckClient({
|
|
63
|
+
resources: resourcesArray,
|
|
64
|
+
appName: appName || process.env.BUILD_APP_NAME || 'unknown-app',
|
|
65
|
+
cacheKey,
|
|
66
|
+
config,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Object} options
|
|
72
|
+
* @param {any} options.redisClient
|
|
73
|
+
* @param {string} [options.appName] - For cache key: healthcheck:${appName}
|
|
74
|
+
* @param {string} [options.cacheKey] - Redis key (e.g. 'health:database:status')
|
|
75
|
+
* @param {Object} [options.config]
|
|
76
|
+
* @returns {HealthCheckClient}
|
|
77
|
+
*/
|
|
78
|
+
function createHealthCheckEndpointClient(options) {
|
|
79
|
+
const { redisClient, appName, cacheKey, config } = options
|
|
80
|
+
|
|
81
|
+
if (!redisClient) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'redisClient is required for createHealthCheckEndpointClient'
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return new HealthCheckClient({
|
|
88
|
+
resources: [{ env: 'REDIS_URL', client: redisClient }],
|
|
89
|
+
appName: appName || process.env.BUILD_APP_NAME || 'unknown-app',
|
|
90
|
+
cacheKey,
|
|
91
|
+
config,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {Object} options
|
|
97
|
+
* @param {any} options.redisClient
|
|
98
|
+
* @param {string} [options.cacheKey] - Redis key (e.g. 'health:database:status')
|
|
99
|
+
* @param {string} [options.appName] - For cache key: healthcheck:${appName}
|
|
100
|
+
* @param {Object} [options.config]
|
|
101
|
+
* @returns {(req: any, res: any) => Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
function getHealthCheckStatus(options) {
|
|
104
|
+
const { redisClient, cacheKey, appName, config } = options
|
|
105
|
+
|
|
106
|
+
if (!redisClient) {
|
|
107
|
+
throw new Error('redisClient is required for getHealthCheckStatus')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const client = createHealthCheckEndpointClient({
|
|
111
|
+
redisClient,
|
|
112
|
+
cacheKey,
|
|
113
|
+
appName,
|
|
114
|
+
config,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return client.healthHandler()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
createHealthCheckWorkerClient,
|
|
122
|
+
createHealthCheckEndpointClient,
|
|
123
|
+
getHealthCheckStatus,
|
|
124
|
+
additionalDatabaseUrlsToResources,
|
|
125
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} options
|
|
3
|
+
* @param {{ name: string } | { name: string, url?: string } | { name: string, client: any }}[] options.resources - Must include Redis resource with client
|
|
4
|
+
* @param {string} [options.appName]
|
|
5
|
+
* @param {number} [options.refreshIntervalMs]
|
|
6
|
+
* @param {string} [options.databaseUrl]
|
|
7
|
+
* @param {string} [options.databaseName]
|
|
8
|
+
* @param {Object<string, string>} [options.additionalDatabaseUrls]
|
|
9
|
+
*/
|
|
10
|
+
const { createHealthCheckWorkerClient } = require('./healthCheckUtils')
|
|
11
|
+
const { DEFAULT_HEALTH_CONFIG } = require('./healthCheckClient')
|
|
12
|
+
|
|
13
|
+
export function createHealthCheckWorker(options) {
|
|
14
|
+
const {
|
|
15
|
+
refreshIntervalMs = DEFAULT_HEALTH_CONFIG.checkIntervalMs,
|
|
16
|
+
...workerClientOptions
|
|
17
|
+
} = options
|
|
18
|
+
|
|
19
|
+
const appName =
|
|
20
|
+
workerClientOptions.appName || process.env.BUILD_APP_NAME || 'unknown-app'
|
|
21
|
+
const dynoId = process.env.HOSTNAME || 'unknown-dyno'
|
|
22
|
+
const processType =
|
|
23
|
+
process.env.BUILD_DYNO_PROCESS_TYPE || 'health-check-worker'
|
|
24
|
+
const logValues = process.env.HEALTH_LOG_VALUES === 'true'
|
|
25
|
+
const prefixLogs = `[${processType}] [${appName}] [${dynoId}] [HealthCheck]`
|
|
26
|
+
|
|
27
|
+
const healthCheckClient = createHealthCheckWorkerClient(workerClientOptions)
|
|
28
|
+
|
|
29
|
+
return async function runHealthCheckWorker() {
|
|
30
|
+
console.log(`${prefixLogs} Starting health check worker...`)
|
|
31
|
+
console.log(`${prefixLogs} Refresh interval: ${refreshIntervalMs}ms`)
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await healthCheckClient.refreshCache()
|
|
35
|
+
if (logValues) {
|
|
36
|
+
console.log(`${prefixLogs} Initial health check completed`)
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`${prefixLogs} Initial health check failed:`, err)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const interval = setInterval(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const result = await healthCheckClient.refreshCache()
|
|
45
|
+
if (logValues) {
|
|
46
|
+
console.log(
|
|
47
|
+
`${prefixLogs} Health check refreshed at ${result.lastCheckAt}, status: ${result.status}`
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`${prefixLogs} Health check refresh failed:`, err)
|
|
52
|
+
}
|
|
53
|
+
}, refreshIntervalMs)
|
|
54
|
+
|
|
55
|
+
process.on('SIGTERM', () => {
|
|
56
|
+
console.log(`${prefixLogs} Received SIGTERM, shutting down...`)
|
|
57
|
+
clearInterval(interval)
|
|
58
|
+
healthCheckClient.cleanup().finally(() => {
|
|
59
|
+
process.exit(0)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
process.on('SIGINT', () => {
|
|
64
|
+
console.log(`${prefixLogs} Received SIGINT, shutting down...`)
|
|
65
|
+
clearInterval(interval)
|
|
66
|
+
healthCheckClient.cleanup().finally(() => {
|
|
67
|
+
process.exit(0)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './metrics/baseMetricsClient'
|
|
2
|
+
export * from './metrics/metricsClient'
|
|
3
|
+
export * from './metrics/metricsRedisClient'
|
|
4
|
+
export * from './metrics/metricsQueueRedisClient'
|
|
5
|
+
export * from './metrics/metricsDatabaseClient'
|
|
6
|
+
export * from './health/healthCheckClient'
|
|
7
|
+
export * from './health/healthCheckUtils'
|
|
8
|
+
export * from './health/healthCheckWorker'
|
|
9
|
+
export * from './redisUtils'
|