@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,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getRedisClientType,
|
|
5
|
+
REDIS_V4,
|
|
6
|
+
IOREDIS,
|
|
7
|
+
REDIS_V3
|
|
8
|
+
} = require('../redisUtils');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HealthCheckCache provides a shared cache layer for health check results.
|
|
12
|
+
* It uses Redis if available for cross-process sharing, with graceful fallback
|
|
13
|
+
* to in-memory cache if Redis is not configured or unavailable.
|
|
14
|
+
*/
|
|
15
|
+
class HealthCheckCache {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {any} [options.redisClient]
|
|
19
|
+
* @param {string} [options.cacheKey] - Redis key (e.g. 'health:database:status')
|
|
20
|
+
* @param {string} [options.appName] - Used when cacheKey not set: healthcheck:${appName}
|
|
21
|
+
* @param {number} [options.staleThresholdMs]
|
|
22
|
+
*/
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.redisClient = options.redisClient || null;
|
|
25
|
+
this.appName = options.appName || process.env.BUILD_APP_NAME || 'unknown-app';
|
|
26
|
+
this.staleThresholdMs = options.staleThresholdMs ?? 180_000;
|
|
27
|
+
this.cacheKey = options.cacheKey || `healthcheck:${this.appName}`;
|
|
28
|
+
|
|
29
|
+
/** In-memory fallback cache */
|
|
30
|
+
this._memoryCache = null;
|
|
31
|
+
this._memoryCacheTimestamp = null;
|
|
32
|
+
if (this.redisClient) {
|
|
33
|
+
this._redisClientType = getRedisClientType(this.redisClient);
|
|
34
|
+
this._redisAvailable = true;
|
|
35
|
+
} else {
|
|
36
|
+
this._redisAvailable = false;
|
|
37
|
+
console.warn(`[HealthCheckCache] Redis not configured for ${this.appName}, using in-memory cache only (not shared across processes)`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if Redis is available and working.
|
|
43
|
+
* @returns {Promise<boolean>}
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
async _checkRedisAvailable() {
|
|
47
|
+
if (!this.redisClient || !this._redisAvailable) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
let pong;
|
|
52
|
+
if (this._redisClientType === REDIS_V3) {
|
|
53
|
+
pong = await new Promise((resolve, reject) => {
|
|
54
|
+
this.redisClient.ping((err, result) => {
|
|
55
|
+
if (err) reject(err);else resolve(result);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
} else if (this._redisClientType === REDIS_V4 || this._redisClientType === IOREDIS) {
|
|
59
|
+
pong = await this.redisClient.ping();
|
|
60
|
+
} else {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return pong === 'PONG';
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (this._redisAvailable) {
|
|
66
|
+
console.warn(`[HealthCheckCache] Redis became unavailable: ${err.message}`);
|
|
67
|
+
this._redisAvailable = false;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets cached health check result from Redis (if available) or in-memory cache.
|
|
75
|
+
* Throws error if Redis is configured but read fails (so caller can return proper error format).
|
|
76
|
+
* @returns {Promise<Object | null>} Cached result or null
|
|
77
|
+
* @throws {Error} If Redis is configured but read fails
|
|
78
|
+
*/
|
|
79
|
+
async get() {
|
|
80
|
+
if (this.redisClient) {
|
|
81
|
+
try {
|
|
82
|
+
let cachedStr;
|
|
83
|
+
if (this._redisClientType === REDIS_V3) {
|
|
84
|
+
cachedStr = await new Promise((resolve, reject) => {
|
|
85
|
+
this.redisClient.get(this.cacheKey, (err, result) => {
|
|
86
|
+
if (err) reject(err);else resolve(result);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
} else if (this._redisClientType === REDIS_V4 || this._redisClientType === IOREDIS) {
|
|
90
|
+
cachedStr = await this.redisClient.get(this.cacheKey);
|
|
91
|
+
}
|
|
92
|
+
if (cachedStr) {
|
|
93
|
+
try {
|
|
94
|
+
const cached = JSON.parse(cachedStr);
|
|
95
|
+
if (cached.result && cached.timestamp) {
|
|
96
|
+
this._memoryCache = cached.result;
|
|
97
|
+
this._memoryCacheTimestamp = cached.timestamp;
|
|
98
|
+
return cached.result;
|
|
99
|
+
}
|
|
100
|
+
} catch (parseErr) {
|
|
101
|
+
console.warn(`[HealthCheckCache] Failed to parse Redis cache:`, parseErr.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
} catch (redisErr) {
|
|
106
|
+
this._redisAvailable = false;
|
|
107
|
+
throw new Error(`Redis cache read failed: ${redisErr.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (this._memoryCache && this._memoryCacheTimestamp) {
|
|
111
|
+
return this._memoryCache;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Sets cached health check result in Redis (if available) and in-memory.
|
|
118
|
+
* @param {Object} result - Health check result to cache
|
|
119
|
+
* @returns {Promise<void>}
|
|
120
|
+
*/
|
|
121
|
+
async set(result) {
|
|
122
|
+
const cacheData = {
|
|
123
|
+
result,
|
|
124
|
+
timestamp: Date.now()
|
|
125
|
+
};
|
|
126
|
+
this._memoryCache = result;
|
|
127
|
+
this._memoryCacheTimestamp = cacheData.timestamp;
|
|
128
|
+
if (await this._checkRedisAvailable()) {
|
|
129
|
+
try {
|
|
130
|
+
const cacheStr = JSON.stringify(cacheData);
|
|
131
|
+
const ttlSeconds = Math.ceil(this.staleThresholdMs / 1000) + 60;
|
|
132
|
+
if (this._redisClientType === REDIS_V3) {
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr, err => {
|
|
135
|
+
if (err) reject(err);else resolve();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
} else if (this._redisClientType === REDIS_V4) {
|
|
139
|
+
await this.redisClient.set(this.cacheKey, cacheStr, {
|
|
140
|
+
EX: ttlSeconds
|
|
141
|
+
});
|
|
142
|
+
} else if (this._redisClientType === IOREDIS) {
|
|
143
|
+
await this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr);
|
|
144
|
+
}
|
|
145
|
+
} catch (redisErr) {
|
|
146
|
+
console.warn(`[HealthCheckCache] Redis write failed (in-memory cache updated):`, redisErr.message);
|
|
147
|
+
this._redisAvailable = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clears the cache (both Redis and in-memory).
|
|
154
|
+
* @returns {Promise<void>}
|
|
155
|
+
*/
|
|
156
|
+
async clear() {
|
|
157
|
+
this._memoryCache = null;
|
|
158
|
+
this._memoryCacheTimestamp = null;
|
|
159
|
+
if (await this._checkRedisAvailable()) {
|
|
160
|
+
try {
|
|
161
|
+
if (this._redisClientType === REDIS_V3) {
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
this.redisClient.del(this.cacheKey, err => {
|
|
164
|
+
if (err) reject(err);else resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
} else if (this._redisClientType === REDIS_V4 || this._redisClientType === IOREDIS) {
|
|
168
|
+
await this.redisClient.del(this.cacheKey);
|
|
169
|
+
}
|
|
170
|
+
} catch (redisErr) {
|
|
171
|
+
console.warn(`[HealthCheckCache] Redis clear failed:`, redisErr.message);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Checks if Redis is configured and available.
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
isRedisAvailable() {
|
|
181
|
+
return this._redisAvailable && this.redisClient !== null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
module.exports = {
|
|
185
|
+
HealthCheckCache
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=healthCheckCache.js.map
|
|
@@ -0,0 +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","staleThresholdMs","cacheKey","_memoryCache","_memoryCacheTimestamp","_redisClientType","_redisAvailable","console","warn","_checkRedisAvailable","pong","Promise","resolve","reject","ping","err","result","message","get","cachedStr","cached","JSON","parse","timestamp","parseErr","redisErr","Error","set","cacheData","Date","now","cacheStr","stringify","ttlSeconds","Math","ceil","setex","EX","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]\n * @param {string} [options.cacheKey] - Redis key (e.g. 'health:database:status')\n * @param {string} [options.appName] - Used when cacheKey not set: healthcheck:${appName}\n * @param {number} [options.staleThresholdMs]\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.staleThresholdMs = options.staleThresholdMs ?? 180_000\n this.cacheKey = options.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 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 (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 this._memoryCache = cached.result\n this._memoryCacheTimestamp = cached.timestamp\n return cached.result\n }\n } catch (parseErr) {\n console.warn(\n `[HealthCheckCache] Failed to parse Redis cache:`,\n parseErr.message\n )\n }\n }\n return null\n } catch (redisErr) {\n this._redisAvailable = false\n throw new Error(`Redis cache read failed: ${redisErr.message}`)\n }\n }\n\n if (this._memoryCache && this._memoryCacheTimestamp) {\n return this._memoryCache\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 this._memoryCache = result\n this._memoryCacheTimestamp = cacheData.timestamp\n\n if (await this._checkRedisAvailable()) {\n try {\n const cacheStr = JSON.stringify(cacheData)\n const ttlSeconds = Math.ceil(this.staleThresholdMs / 1000) + 60\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 (this._redisClientType === REDIS_V4) {\n await this.redisClient.set(this.cacheKey, cacheStr, {\n EX: ttlSeconds,\n })\n } else if (this._redisClientType === IOREDIS) {\n await this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr)\n }\n } catch (redisErr) {\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":[]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export type HealthResource = {
|
|
2
|
+
env: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
} | {
|
|
5
|
+
env: string;
|
|
6
|
+
client?: any;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ env: string, url?: string } | { env: string, client?: any }} HealthResource
|
|
10
|
+
*/
|
|
11
|
+
export class HealthCheckClient {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {HealthResource[]} options.resources - Must include Redis resource with client
|
|
15
|
+
* @param {Object} [options.config]
|
|
16
|
+
* @param {string} [options.appName] - For cache key: healthcheck:${appName}
|
|
17
|
+
* @param {string} [options.cacheKey] - Redis key (overrides appName)
|
|
18
|
+
*/
|
|
19
|
+
constructor(options?: {
|
|
20
|
+
resources: HealthResource[];
|
|
21
|
+
config?: Object | undefined;
|
|
22
|
+
appName?: string | undefined;
|
|
23
|
+
cacheKey?: string | undefined;
|
|
24
|
+
});
|
|
25
|
+
healthConfig: {
|
|
26
|
+
constructor?: Function | undefined;
|
|
27
|
+
toString?: (() => string) | undefined;
|
|
28
|
+
toLocaleString?: (() => string) | undefined;
|
|
29
|
+
valueOf?: (() => Object) | undefined;
|
|
30
|
+
hasOwnProperty?: ((v: PropertyKey) => boolean) | undefined;
|
|
31
|
+
isPrototypeOf?: ((v: Object) => boolean) | undefined;
|
|
32
|
+
propertyIsEnumerable?: ((v: PropertyKey) => boolean) | undefined;
|
|
33
|
+
checkIntervalMs: number;
|
|
34
|
+
staleThresholdMs: number;
|
|
35
|
+
checkTimeoutMs: number;
|
|
36
|
+
maxDbConnectLatencyMs: number;
|
|
37
|
+
};
|
|
38
|
+
appName: string;
|
|
39
|
+
prefixLogs: string;
|
|
40
|
+
_refreshPromise: any;
|
|
41
|
+
/** @type {Map<string, { pool: any, type: string }>} */
|
|
42
|
+
_databasePools: Map<string, {
|
|
43
|
+
pool: any;
|
|
44
|
+
type: string;
|
|
45
|
+
}>;
|
|
46
|
+
/** @type {HealthResource[]} */
|
|
47
|
+
_resources: HealthResource[];
|
|
48
|
+
_redisClientType: string | undefined;
|
|
49
|
+
_cache: HealthCheckCache;
|
|
50
|
+
_getEnv(resource: any): any;
|
|
51
|
+
_getRedisClientForCache(): any;
|
|
52
|
+
_getPool(env: any, url: any): {
|
|
53
|
+
pool: any;
|
|
54
|
+
type: string;
|
|
55
|
+
} | undefined;
|
|
56
|
+
_checkDatabase(resource: any): Promise<{
|
|
57
|
+
status: string;
|
|
58
|
+
connectMs: number;
|
|
59
|
+
} | {
|
|
60
|
+
connectMs?: any;
|
|
61
|
+
status: string;
|
|
62
|
+
error: string;
|
|
63
|
+
}>;
|
|
64
|
+
_checkRedis(resource: any): Promise<{
|
|
65
|
+
status: string;
|
|
66
|
+
error?: undefined;
|
|
67
|
+
} | {
|
|
68
|
+
status: string;
|
|
69
|
+
error: string;
|
|
70
|
+
}>;
|
|
71
|
+
_performHealthCheckInternal(): Promise<{
|
|
72
|
+
status: string;
|
|
73
|
+
lastCheckAt: number;
|
|
74
|
+
resources: {};
|
|
75
|
+
isStale: boolean;
|
|
76
|
+
config: {
|
|
77
|
+
constructor?: Function | undefined;
|
|
78
|
+
toString?: (() => string) | undefined;
|
|
79
|
+
toLocaleString?: (() => string) | undefined;
|
|
80
|
+
valueOf?: (() => Object) | undefined;
|
|
81
|
+
hasOwnProperty?: ((v: PropertyKey) => boolean) | undefined;
|
|
82
|
+
isPrototypeOf?: ((v: Object) => boolean) | undefined;
|
|
83
|
+
propertyIsEnumerable?: ((v: PropertyKey) => boolean) | undefined;
|
|
84
|
+
checkIntervalMs: number;
|
|
85
|
+
staleThresholdMs: number;
|
|
86
|
+
checkTimeoutMs: number;
|
|
87
|
+
maxDbConnectLatencyMs: number;
|
|
88
|
+
};
|
|
89
|
+
}>;
|
|
90
|
+
_formatResult(result: any, cached?: boolean): any;
|
|
91
|
+
performHealthCheck(): Promise<any>;
|
|
92
|
+
getCachedResult(): Promise<any>;
|
|
93
|
+
refreshCache(): Promise<{
|
|
94
|
+
status: string;
|
|
95
|
+
lastCheckAt: number;
|
|
96
|
+
resources: {};
|
|
97
|
+
isStale: boolean;
|
|
98
|
+
config: {
|
|
99
|
+
constructor?: Function | undefined;
|
|
100
|
+
toString?: (() => string) | undefined;
|
|
101
|
+
toLocaleString?: (() => string) | undefined;
|
|
102
|
+
valueOf?: (() => Object) | undefined;
|
|
103
|
+
hasOwnProperty?: ((v: PropertyKey) => boolean) | undefined;
|
|
104
|
+
isPrototypeOf?: ((v: Object) => boolean) | undefined;
|
|
105
|
+
propertyIsEnumerable?: ((v: PropertyKey) => boolean) | undefined;
|
|
106
|
+
checkIntervalMs: number;
|
|
107
|
+
staleThresholdMs: number;
|
|
108
|
+
checkTimeoutMs: number;
|
|
109
|
+
maxDbConnectLatencyMs: number;
|
|
110
|
+
};
|
|
111
|
+
}>;
|
|
112
|
+
clearCache(): void;
|
|
113
|
+
healthHandler(): (req: any, res: any) => Promise<void>;
|
|
114
|
+
cleanup(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
/** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number, maxDbConnectLatencyMs: number }} */
|
|
117
|
+
export const DEFAULT_HEALTH_CONFIG: {
|
|
118
|
+
checkIntervalMs: number;
|
|
119
|
+
staleThresholdMs: number;
|
|
120
|
+
checkTimeoutMs: number;
|
|
121
|
+
maxDbConnectLatencyMs: number;
|
|
122
|
+
};
|
|
123
|
+
import { HealthCheckCache } from "./healthCheckCache";
|
|
124
|
+
//# sourceMappingURL=healthCheckClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../../src/health/healthCheckClient.js"],"names":[],"mappings":"6BA6Ea;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAA;CAAE;AAD1E;;GAEG;AACH;IACE;;;;;;OAMG;IACH;QALqC,SAAS,EAAnC,cAAc,EAAE;QACC,MAAM;QACN,OAAO;QACP,QAAQ;OA2BnC;IAxBC;;;;;;;;;;;;MAAmE;IACnE,gBACgE;IAEhE,mBAAmD;IAEnD,qBAA2B;IAC3B,uDAAuD;IACvD;cAD+B,GAAG;cAAQ,MAAM;OACjB;IAE/B,+BAA+B;IAC/B,YADW,cAAc,EAAE,CACc;IAIvC,qCAAuD;IAGzD,yBAKE;IAGJ,4BAEC;IAED,+BAGC;IAED;cA5BiC,GAAG;cAAQ,MAAM;kBAsCjD;IAED;;;;;;;OAoCC;IAED;;;;;;OA4BC;IAED;;;;;;;;;;;;;;;;;;OA8BC;IAED,kDAeC;IAED,mCAgBC;IAED,gCAiBC;IAED;;;;;;;;;;;;;;;;;;OAIC;IAED,mBAEC;IAED,uDAiCC;IAED,yBASC;CACF;AAhUD,2HAA2H;AAC3H,oCADW;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,qBAAqB,EAAE,MAAM,CAAA;CAAE,CAOtH"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createDatabasePool,
|
|
5
|
+
runHealthCheck,
|
|
6
|
+
closePool
|
|
7
|
+
} = require('./databaseChecker');
|
|
8
|
+
const {
|
|
9
|
+
getRedisClientType,
|
|
10
|
+
REDIS_V4,
|
|
11
|
+
IOREDIS,
|
|
12
|
+
REDIS_V3
|
|
13
|
+
} = require('../redisUtils');
|
|
14
|
+
const {
|
|
15
|
+
HealthCheckCache
|
|
16
|
+
} = require('./healthCheckCache');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} name
|
|
20
|
+
* @returns {number | undefined}
|
|
21
|
+
*/
|
|
22
|
+
function readNumberEnv(name) {
|
|
23
|
+
const raw = process.env[name];
|
|
24
|
+
if (raw == null || raw === '') return undefined;
|
|
25
|
+
const num = Number(raw);
|
|
26
|
+
return Number.isFinite(num) ? num : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number, maxDbConnectLatencyMs: number }} */
|
|
30
|
+
const DEFAULT_HEALTH_CONFIG = {
|
|
31
|
+
checkIntervalMs: 30_000,
|
|
32
|
+
staleThresholdMs: 180_000,
|
|
33
|
+
checkTimeoutMs: 15_000,
|
|
34
|
+
maxDbConnectLatencyMs: readNumberEnv('HEALTH_DB_MAX_CONNECT_LATENCY_MS') ?? 1000
|
|
35
|
+
};
|
|
36
|
+
const SENSITIVE_PATTERNS = [{
|
|
37
|
+
pattern: /(postgres(?:ql)?|mysql|mongodb|redis|amqp):\/\/([^:]+):([^@]+)@([^:/]+)(:\d+)?\/([^\s?]+)/gi,
|
|
38
|
+
replacement: '$1://***:***@***$5/***'
|
|
39
|
+
}, {
|
|
40
|
+
pattern: /(\w+):\/\/([^:]+):([^@]+)@([^\s/]+)/gi,
|
|
41
|
+
replacement: '$1://***:***@***'
|
|
42
|
+
}, {
|
|
43
|
+
pattern: /(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
44
|
+
replacement: '$1=***'
|
|
45
|
+
}, {
|
|
46
|
+
pattern: /(database|table|schema|role|user|relation|column|index)\s*["']([^"']+)["']/gi,
|
|
47
|
+
replacement: '$1 "***"'
|
|
48
|
+
}, {
|
|
49
|
+
pattern: /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?\b/g,
|
|
50
|
+
replacement: '***$2'
|
|
51
|
+
}, {
|
|
52
|
+
pattern: /\b(host|hostname|server)["\s]*[:=]["\s]*([^\s,}"]+)/gi,
|
|
53
|
+
replacement: '$1=***'
|
|
54
|
+
}];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} text
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function maskSensitiveData(text) {
|
|
61
|
+
if (!text || typeof text !== 'string') return text;
|
|
62
|
+
let masked = text;
|
|
63
|
+
for (const {
|
|
64
|
+
pattern,
|
|
65
|
+
replacement
|
|
66
|
+
} of SENSITIVE_PATTERNS) {
|
|
67
|
+
masked = masked.replace(pattern, replacement);
|
|
68
|
+
}
|
|
69
|
+
return masked;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {{ env: string, url?: string } | { env: string, client?: any }} HealthResource
|
|
74
|
+
*/
|
|
75
|
+
class HealthCheckClient {
|
|
76
|
+
/**
|
|
77
|
+
* @param {Object} options
|
|
78
|
+
* @param {HealthResource[]} options.resources - Must include Redis resource with client
|
|
79
|
+
* @param {Object} [options.config]
|
|
80
|
+
* @param {string} [options.appName] - For cache key: healthcheck:${appName}
|
|
81
|
+
* @param {string} [options.cacheKey] - Redis key (overrides appName)
|
|
82
|
+
*/
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.healthConfig = {
|
|
85
|
+
...DEFAULT_HEALTH_CONFIG,
|
|
86
|
+
...options.config
|
|
87
|
+
};
|
|
88
|
+
this.appName = options.appName || process.env.BUILD_APP_NAME || 'unknown-app';
|
|
89
|
+
this.prefixLogs = `[${this.appName}] [HealthCheck]`;
|
|
90
|
+
this._refreshPromise = null;
|
|
91
|
+
/** @type {Map<string, { pool: any, type: string }>} */
|
|
92
|
+
this._databasePools = new Map();
|
|
93
|
+
|
|
94
|
+
/** @type {HealthResource[]} */
|
|
95
|
+
this._resources = options.resources || [];
|
|
96
|
+
const redisClient = this._getRedisClientForCache();
|
|
97
|
+
if (redisClient) {
|
|
98
|
+
this._redisClientType = getRedisClientType(redisClient);
|
|
99
|
+
}
|
|
100
|
+
this._cache = new HealthCheckCache({
|
|
101
|
+
redisClient: redisClient || null,
|
|
102
|
+
cacheKey: options.cacheKey,
|
|
103
|
+
appName: this.appName,
|
|
104
|
+
staleThresholdMs: this.healthConfig.staleThresholdMs
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
_getEnv(resource) {
|
|
108
|
+
return resource.env ?? resource.name;
|
|
109
|
+
}
|
|
110
|
+
_getRedisClientForCache() {
|
|
111
|
+
const redisResource = this._resources.find(r => 'client' in r && r.client);
|
|
112
|
+
return redisResource?.client || null;
|
|
113
|
+
}
|
|
114
|
+
_getPool(env, url) {
|
|
115
|
+
if (!this._databasePools.has(env)) {
|
|
116
|
+
const {
|
|
117
|
+
pool,
|
|
118
|
+
type
|
|
119
|
+
} = createDatabasePool(env, url, this.healthConfig.checkTimeoutMs);
|
|
120
|
+
this._databasePools.set(env, {
|
|
121
|
+
pool,
|
|
122
|
+
type
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return this._databasePools.get(env);
|
|
126
|
+
}
|
|
127
|
+
async _checkDatabase(resource) {
|
|
128
|
+
const env = this._getEnv(resource);
|
|
129
|
+
const url = 'url' in resource ? resource.url : process.env[env];
|
|
130
|
+
if (!url) {
|
|
131
|
+
return {
|
|
132
|
+
status: 'error',
|
|
133
|
+
error: `Env ${env} not set`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const {
|
|
138
|
+
pool,
|
|
139
|
+
type
|
|
140
|
+
} = this._getPool(env, url);
|
|
141
|
+
const timings = await runHealthCheck(pool, type);
|
|
142
|
+
const maxConnect = this.healthConfig.maxDbConnectLatencyMs;
|
|
143
|
+
if (typeof maxConnect === 'number' && Number.isFinite(maxConnect) && timings?.connectMs != null && timings.connectMs > maxConnect) {
|
|
144
|
+
return {
|
|
145
|
+
status: 'error',
|
|
146
|
+
error: `DB connect latency ${timings.connectMs}ms exceeds ${maxConnect}ms`,
|
|
147
|
+
connectMs: timings.connectMs
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
status: 'ok',
|
|
152
|
+
connectMs: timings.connectMs
|
|
153
|
+
};
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const timings = err?.healthCheckTimings;
|
|
156
|
+
return {
|
|
157
|
+
status: 'error',
|
|
158
|
+
error: maskSensitiveData(err.message),
|
|
159
|
+
...(timings && typeof timings === 'object' ? {
|
|
160
|
+
connectMs: timings.connectMs
|
|
161
|
+
} : {})
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async _checkRedis(resource) {
|
|
166
|
+
const {
|
|
167
|
+
client
|
|
168
|
+
} = resource;
|
|
169
|
+
if (!client) return {
|
|
170
|
+
status: 'ok'
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
let pong;
|
|
174
|
+
if (this._redisClientType === REDIS_V3) {
|
|
175
|
+
pong = await new Promise((resolve, reject) => {
|
|
176
|
+
client.ping((err, result) => {
|
|
177
|
+
if (err) reject(err);else resolve(result);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
} else if (this._redisClientType === REDIS_V4 || this._redisClientType === IOREDIS) {
|
|
181
|
+
pong = await client.ping();
|
|
182
|
+
} else {
|
|
183
|
+
return {
|
|
184
|
+
status: 'error',
|
|
185
|
+
error: 'Unknown Redis client type'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return pong === 'PONG' ? {
|
|
189
|
+
status: 'ok'
|
|
190
|
+
} : {
|
|
191
|
+
status: 'error',
|
|
192
|
+
error: `Unexpected: ${pong}`
|
|
193
|
+
};
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
status: 'error',
|
|
197
|
+
error: maskSensitiveData(err.message)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async _performHealthCheckInternal() {
|
|
202
|
+
const resources = {};
|
|
203
|
+
for (const resource of this._resources) {
|
|
204
|
+
const env = this._getEnv(resource);
|
|
205
|
+
if ('client' in resource && resource.client) {
|
|
206
|
+
resources[env] = await this._checkRedis(resource);
|
|
207
|
+
} else {
|
|
208
|
+
resources[env] = await this._checkDatabase(resource);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const sortedResources = Object.keys(resources).sort().reduce((acc, key) => {
|
|
212
|
+
acc[key] = resources[key];
|
|
213
|
+
return acc;
|
|
214
|
+
}, {});
|
|
215
|
+
const hasError = Object.values(resources).some(r => r.status === 'error');
|
|
216
|
+
const lastCheckAt = Date.now();
|
|
217
|
+
return {
|
|
218
|
+
status: hasError ? 'error' : 'ok',
|
|
219
|
+
lastCheckAt,
|
|
220
|
+
resources: sortedResources,
|
|
221
|
+
isStale: false,
|
|
222
|
+
config: this.healthConfig
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
_formatResult(result, cached = false) {
|
|
226
|
+
const isStale = !result.lastCheckAt || Date.now() - result.lastCheckAt > this.healthConfig.staleThresholdMs;
|
|
227
|
+
return {
|
|
228
|
+
...result,
|
|
229
|
+
isStale,
|
|
230
|
+
status: isStale ? 'stale' : result.status,
|
|
231
|
+
...(isStale && {
|
|
232
|
+
error: 'Health check data is stale, health-check worker may not be running. Resource statuses are unknown.'
|
|
233
|
+
}),
|
|
234
|
+
...(cached && {
|
|
235
|
+
cached: true
|
|
236
|
+
})
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async performHealthCheck() {
|
|
240
|
+
if (this._refreshPromise) {
|
|
241
|
+
return this._refreshPromise;
|
|
242
|
+
}
|
|
243
|
+
this._refreshPromise = this._performHealthCheckInternal().then(result => {
|
|
244
|
+
this._refreshPromise = null;
|
|
245
|
+
return this._formatResult(result);
|
|
246
|
+
}).catch(err => {
|
|
247
|
+
this._refreshPromise = null;
|
|
248
|
+
throw err;
|
|
249
|
+
});
|
|
250
|
+
return this._refreshPromise;
|
|
251
|
+
}
|
|
252
|
+
async getCachedResult() {
|
|
253
|
+
try {
|
|
254
|
+
const cached = await this._cache.get();
|
|
255
|
+
if (cached) return this._formatResult(cached);
|
|
256
|
+
return null;
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(`${this.prefixLogs} Failed to read from cache:`, err);
|
|
259
|
+
return {
|
|
260
|
+
status: 'error',
|
|
261
|
+
lastCheckAt: null,
|
|
262
|
+
resources: {},
|
|
263
|
+
isStale: true,
|
|
264
|
+
error: 'Redis unavailable, unable to read health status of other resources',
|
|
265
|
+
config: this.healthConfig
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async refreshCache() {
|
|
270
|
+
const result = await this._performHealthCheckInternal();
|
|
271
|
+
await this._cache.set(result);
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
clearCache() {
|
|
275
|
+
this._refreshPromise = null;
|
|
276
|
+
}
|
|
277
|
+
healthHandler() {
|
|
278
|
+
return async (req, res) => {
|
|
279
|
+
try {
|
|
280
|
+
const result = await this.getCachedResult();
|
|
281
|
+
if (!result) {
|
|
282
|
+
res.status(503).json({
|
|
283
|
+
status: 'error',
|
|
284
|
+
lastCheckAt: null,
|
|
285
|
+
resources: {},
|
|
286
|
+
isStale: true,
|
|
287
|
+
error: 'No health check data yet, health-check worker may not be running',
|
|
288
|
+
config: this.healthConfig
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const statusCode = result.status === 'ok' ? 200 : 503;
|
|
293
|
+
res.status(statusCode).json(result);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
console.error(`${this.prefixLogs} Health check failed:`, err);
|
|
296
|
+
res.status(503).json({
|
|
297
|
+
status: 'error',
|
|
298
|
+
lastCheckAt: null,
|
|
299
|
+
resources: {},
|
|
300
|
+
isStale: true,
|
|
301
|
+
error: 'Redis unavailable, unable to read health status of other resources',
|
|
302
|
+
config: this.healthConfig
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async cleanup() {
|
|
308
|
+
for (const [, {
|
|
309
|
+
pool
|
|
310
|
+
}] of this._databasePools) {
|
|
311
|
+
try {
|
|
312
|
+
await closePool(pool);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error(`${this.prefixLogs} Error closing database pool:`, err);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
this._databasePools.clear();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
module.exports = {
|
|
321
|
+
HealthCheckClient,
|
|
322
|
+
DEFAULT_HEALTH_CONFIG
|
|
323
|
+
};
|
|
324
|
+
//# sourceMappingURL=healthCheckClient.js.map
|