@adalo/metrics 0.1.149 → 0.1.150
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 +1 -0
- package/lib/health/databaseChecker.d.ts +6 -2
- package/lib/health/databaseChecker.d.ts.map +1 -1
- package/lib/health/databaseChecker.js +78 -4
- package/lib/health/databaseChecker.js.map +1 -1
- package/lib/health/healthCheckClient.d.ts +6 -8
- package/lib/health/healthCheckClient.d.ts.map +1 -1
- package/lib/health/healthCheckClient.js +28 -5
- package/lib/health/healthCheckClient.js.map +1 -1
- package/package.json +1 -1
- package/src/health/databaseChecker.js +75 -4
- package/src/health/healthCheckClient.js +33 -3
package/README-health.md
CHANGED
|
@@ -26,6 +26,7 @@ preventing HTTP requests from triggering database queries.
|
|
|
26
26
|
| `HOSTNAME` | Dyno/instance ID for logging | `unknown-dyno` |
|
|
27
27
|
| `BUILD_DYNO_PROCESS_TYPE` | Process type for logging | `health-check-worker` |
|
|
28
28
|
| `HEALTH_LOG_VALUES` | Enable logging of health check refresh status (`true`/`false`) | `false` |
|
|
29
|
+
| `HEALTH_DB_MAX_CONNECT_LATENCY_MS` | Fail DB health if connection acquisition/connect time exceeds this (ms) | `1000` |
|
|
29
30
|
| `REDIS_URL` | Redis connection URL for cache | - |
|
|
30
31
|
| `DATABASE_URL` | Main PostgreSQL connection URL (for backend worker) | - |
|
|
31
32
|
| `META_DB_URL` | Main PostgreSQL connection URL (for database worker) | - |
|
|
@@ -28,9 +28,13 @@ export function createDatabasePool(env: string, url: string, connectionTimeoutMs
|
|
|
28
28
|
/**
|
|
29
29
|
* @param {any} pool
|
|
30
30
|
* @param {string} type
|
|
31
|
-
* @returns {Promise<
|
|
31
|
+
* @returns {Promise<{ connectMs: number, queryMs: number, totalMs: number }>}
|
|
32
32
|
*/
|
|
33
|
-
export function runHealthCheck(pool: any, type: string): Promise<
|
|
33
|
+
export function runHealthCheck(pool: any, type: string): Promise<{
|
|
34
|
+
connectMs: number;
|
|
35
|
+
queryMs: number;
|
|
36
|
+
totalMs: number;
|
|
37
|
+
}>;
|
|
34
38
|
/**
|
|
35
39
|
* @param {any} pool
|
|
36
40
|
* @returns {Promise<void>}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"databaseChecker.d.ts","sourceRoot":"","sources":["../../src/health/databaseChecker.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"databaseChecker.d.ts","sourceRoot":"","sources":["../../src/health/databaseChecker.js"],"names":[],"mappings":"AAaA;;;GAGG;AACH,qCAHW,MAAM,GACJ,MAAM,CAYlB;AAED;;;;GAIG;AACH,wCAJW,MAAM,8BAEJ;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAiBnG;AAED;;;;;GAKG;AACH,wCALW,MAAM,OACN,MAAM,uBACN,MAAM;UACI,GAAG;UAAQ,MAAM;EA8BrC;AAED;;;;GAIG;AACH,qCAJW,GAAG,QACH,MAAM;eACiB,MAAM;aAAW,MAAM;aAAW,MAAM;GAwEzE;AAED;;;GAGG;AACH,gCAHW,GAAG,GACD,QAAQ,IAAI,CAAC,CAQzB;AA3KD,0CAAmC;AACnC,oCAA6B"}
|
|
@@ -7,6 +7,13 @@ const mysql = require('mysql2/promise');
|
|
|
7
7
|
const DB_TYPE_POSTGRES = 'postgres';
|
|
8
8
|
const DB_TYPE_MYSQL = 'mysql';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
function nowMs() {
|
|
14
|
+
return Number(process.hrtime.bigint() / 1_000_000n);
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* @param {string} url
|
|
12
19
|
* @returns {string} postgres | mysql
|
|
@@ -88,14 +95,81 @@ function createDatabasePool(env, url, connectionTimeoutMs) {
|
|
|
88
95
|
/**
|
|
89
96
|
* @param {any} pool
|
|
90
97
|
* @param {string} type
|
|
91
|
-
* @returns {Promise<
|
|
98
|
+
* @returns {Promise<{ connectMs: number, queryMs: number, totalMs: number }>}
|
|
92
99
|
*/
|
|
93
100
|
async function runHealthCheck(pool, type) {
|
|
94
101
|
if (type === DB_TYPE_MYSQL) {
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
const startTotal = nowMs();
|
|
103
|
+
let connectMs = 0;
|
|
104
|
+
let queryMs = 0;
|
|
105
|
+
|
|
106
|
+
/** @type {any} */
|
|
107
|
+
let conn;
|
|
108
|
+
try {
|
|
109
|
+
const startConnect = nowMs();
|
|
110
|
+
conn = await pool.getConnection();
|
|
111
|
+
connectMs = nowMs() - startConnect;
|
|
112
|
+
const startQuery = nowMs();
|
|
113
|
+
await conn.query('SELECT 1');
|
|
114
|
+
queryMs = nowMs() - startQuery;
|
|
115
|
+
return {
|
|
116
|
+
connectMs,
|
|
117
|
+
queryMs,
|
|
118
|
+
totalMs: nowMs() - startTotal
|
|
119
|
+
};
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const totalMs = nowMs() - startTotal;
|
|
122
|
+
err.healthCheckTimings = {
|
|
123
|
+
connectMs,
|
|
124
|
+
queryMs,
|
|
125
|
+
totalMs
|
|
126
|
+
};
|
|
127
|
+
throw err;
|
|
128
|
+
} finally {
|
|
129
|
+
if (conn) {
|
|
130
|
+
try {
|
|
131
|
+
conn.release();
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const startTotal = nowMs();
|
|
139
|
+
let connectMs = 0;
|
|
140
|
+
let queryMs = 0;
|
|
141
|
+
|
|
142
|
+
/** @type {import('pg').PoolClient | null} */
|
|
143
|
+
let client = null;
|
|
144
|
+
try {
|
|
145
|
+
const startConnect = nowMs();
|
|
146
|
+
client = await pool.connect();
|
|
147
|
+
connectMs = nowMs() - startConnect;
|
|
148
|
+
const startQuery = nowMs();
|
|
149
|
+
await client.query('SELECT 1');
|
|
150
|
+
queryMs = nowMs() - startQuery;
|
|
151
|
+
return {
|
|
152
|
+
connectMs,
|
|
153
|
+
queryMs,
|
|
154
|
+
totalMs: nowMs() - startTotal
|
|
155
|
+
};
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const totalMs = nowMs() - startTotal;
|
|
158
|
+
err.healthCheckTimings = {
|
|
159
|
+
connectMs,
|
|
160
|
+
queryMs,
|
|
161
|
+
totalMs
|
|
162
|
+
};
|
|
163
|
+
throw err;
|
|
164
|
+
} finally {
|
|
165
|
+
if (client) {
|
|
166
|
+
try {
|
|
167
|
+
client.release();
|
|
168
|
+
} catch {
|
|
169
|
+
// ignore
|
|
170
|
+
}
|
|
171
|
+
}
|
|
97
172
|
}
|
|
98
|
-
await pool.query('SELECT 1');
|
|
99
173
|
}
|
|
100
174
|
|
|
101
175
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"databaseChecker.js","names":["Pool","require","mysql","DB_TYPE_POSTGRES","DB_TYPE_MYSQL","getDatabaseType","url","lower","toLowerCase","startsWith","parseConnectionUrl","type","parsed","URL","defaultPort","defaultDb","host","hostname","port","parseInt","String","user","username","password","database","pathname","replace","createDatabasePool","env","connectionTimeoutMs","config","Error","pool","createPool","connectionLimit","connectTimeout","waitForConnections","connectionString","max","idleTimeoutMillis","connectionTimeoutMillis","runHealthCheck","
|
|
1
|
+
{"version":3,"file":"databaseChecker.js","names":["Pool","require","mysql","DB_TYPE_POSTGRES","DB_TYPE_MYSQL","nowMs","Number","process","hrtime","bigint","getDatabaseType","url","lower","toLowerCase","startsWith","parseConnectionUrl","type","parsed","URL","defaultPort","defaultDb","host","hostname","port","parseInt","String","user","username","password","database","pathname","replace","createDatabasePool","env","connectionTimeoutMs","config","Error","pool","createPool","connectionLimit","connectTimeout","waitForConnections","connectionString","max","idleTimeoutMillis","connectionTimeoutMillis","runHealthCheck","startTotal","connectMs","queryMs","conn","startConnect","getConnection","startQuery","query","totalMs","err","healthCheckTimings","release","client","connect","closePool","end","module","exports"],"sources":["../../src/health/databaseChecker.js"],"sourcesContent":["const { Pool } = require('pg')\nconst mysql = require('mysql2/promise')\n\nconst DB_TYPE_POSTGRES = 'postgres'\nconst DB_TYPE_MYSQL = 'mysql'\n\n/**\n * @returns {number}\n */\nfunction nowMs() {\n return Number(process.hrtime.bigint() / 1_000_000n)\n}\n\n/**\n * @param {string} url\n * @returns {string} postgres | mysql\n */\nfunction getDatabaseType(url) {\n if (!url || typeof url !== 'string') return DB_TYPE_POSTGRES\n const lower = url.toLowerCase()\n if (lower.startsWith('mysql://') || lower.startsWith('mysql2://')) {\n return DB_TYPE_MYSQL\n }\n if (lower.startsWith('mariadb://')) {\n return DB_TYPE_MYSQL\n }\n return DB_TYPE_POSTGRES\n}\n\n/**\n * @param {string} url\n * @param {string} [type]\n * @returns {{ host: string, port: number, user: string, password: string, database: string } | null}\n */\nfunction parseConnectionUrl(url, type) {\n try {\n const parsed = new URL(url)\n const defaultPort = type === DB_TYPE_MYSQL ? 3306 : 5432\n const defaultDb = type === DB_TYPE_MYSQL ? 'mysql' : 'postgres'\n return {\n host: parsed.hostname || 'localhost',\n port: parseInt(parsed.port || String(defaultPort), 10),\n user: parsed.username || '',\n password: parsed.password || '',\n database: (parsed.pathname || '/').replace(/^\\//, '') || defaultDb,\n }\n } catch {\n return null\n }\n}\n\n/**\n * @param {string} env\n * @param {string} url\n * @param {number} connectionTimeoutMs\n * @returns {{ pool: any, type: string }}\n */\nfunction createDatabasePool(env, url, connectionTimeoutMs) {\n const type = getDatabaseType(url)\n\n if (type === DB_TYPE_MYSQL) {\n const config = parseConnectionUrl(url, DB_TYPE_MYSQL)\n if (!config) {\n throw new Error(`Invalid MySQL URL for ${env}`)\n }\n const pool = mysql.createPool({\n host: config.host,\n port: config.port,\n user: config.user,\n password: config.password,\n database: config.database,\n connectionLimit: 1,\n connectTimeout: connectionTimeoutMs,\n waitForConnections: false,\n })\n return { pool, type: DB_TYPE_MYSQL }\n }\n\n const pool = new Pool({\n connectionString: url,\n max: 1,\n idleTimeoutMillis: 30000,\n connectionTimeoutMillis: connectionTimeoutMs,\n })\n return { pool, type: DB_TYPE_POSTGRES }\n}\n\n/**\n * @param {any} pool\n * @param {string} type\n * @returns {Promise<{ connectMs: number, queryMs: number, totalMs: number }>}\n */\nasync function runHealthCheck(pool, type) {\n if (type === DB_TYPE_MYSQL) {\n const startTotal = nowMs()\n let connectMs = 0\n let queryMs = 0\n\n /** @type {any} */\n let conn\n try {\n const startConnect = nowMs()\n conn = await pool.getConnection()\n connectMs = nowMs() - startConnect\n\n const startQuery = nowMs()\n await conn.query('SELECT 1')\n queryMs = nowMs() - startQuery\n\n return { connectMs, queryMs, totalMs: nowMs() - startTotal }\n } catch (err) {\n const totalMs = nowMs() - startTotal\n err.healthCheckTimings = {\n connectMs,\n queryMs,\n totalMs,\n }\n throw err\n } finally {\n if (conn) {\n try {\n conn.release()\n } catch {\n // ignore\n }\n }\n }\n }\n\n const startTotal = nowMs()\n let connectMs = 0\n let queryMs = 0\n\n /** @type {import('pg').PoolClient | null} */\n let client = null\n try {\n const startConnect = nowMs()\n client = await pool.connect()\n connectMs = nowMs() - startConnect\n\n const startQuery = nowMs()\n await client.query('SELECT 1')\n queryMs = nowMs() - startQuery\n\n return { connectMs, queryMs, totalMs: nowMs() - startTotal }\n } catch (err) {\n const totalMs = nowMs() - startTotal\n err.healthCheckTimings = {\n connectMs,\n queryMs,\n totalMs,\n }\n throw err\n } finally {\n if (client) {\n try {\n client.release()\n } catch {\n // ignore\n }\n }\n }\n}\n\n/**\n * @param {any} pool\n * @returns {Promise<void>}\n */\nasync function closePool(pool) {\n try {\n await pool.end()\n } catch {\n // ignore\n }\n}\n\nmodule.exports = {\n getDatabaseType,\n parseConnectionUrl,\n createDatabasePool,\n runHealthCheck,\n closePool,\n DB_TYPE_POSTGRES,\n DB_TYPE_MYSQL,\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -33,6 +33,7 @@ export class HealthCheckClient {
|
|
|
33
33
|
checkIntervalMs: number;
|
|
34
34
|
staleThresholdMs: number;
|
|
35
35
|
checkTimeoutMs: number;
|
|
36
|
+
maxDbConnectLatencyMs: number;
|
|
36
37
|
};
|
|
37
38
|
appName: string;
|
|
38
39
|
prefixLogs: string;
|
|
@@ -52,13 +53,7 @@ export class HealthCheckClient {
|
|
|
52
53
|
pool: any;
|
|
53
54
|
type: string;
|
|
54
55
|
} | undefined;
|
|
55
|
-
_checkDatabase(resource: any): Promise<
|
|
56
|
-
status: string;
|
|
57
|
-
error: string;
|
|
58
|
-
} | {
|
|
59
|
-
status: string;
|
|
60
|
-
error?: undefined;
|
|
61
|
-
}>;
|
|
56
|
+
_checkDatabase(resource: any): Promise<any>;
|
|
62
57
|
_checkRedis(resource: any): Promise<{
|
|
63
58
|
status: string;
|
|
64
59
|
error?: undefined;
|
|
@@ -82,6 +77,7 @@ export class HealthCheckClient {
|
|
|
82
77
|
checkIntervalMs: number;
|
|
83
78
|
staleThresholdMs: number;
|
|
84
79
|
checkTimeoutMs: number;
|
|
80
|
+
maxDbConnectLatencyMs: number;
|
|
85
81
|
};
|
|
86
82
|
}>;
|
|
87
83
|
_formatResult(result: any, cached?: boolean): any;
|
|
@@ -103,17 +99,19 @@ export class HealthCheckClient {
|
|
|
103
99
|
checkIntervalMs: number;
|
|
104
100
|
staleThresholdMs: number;
|
|
105
101
|
checkTimeoutMs: number;
|
|
102
|
+
maxDbConnectLatencyMs: number;
|
|
106
103
|
};
|
|
107
104
|
}>;
|
|
108
105
|
clearCache(): void;
|
|
109
106
|
healthHandler(): (req: any, res: any) => Promise<void>;
|
|
110
107
|
cleanup(): Promise<void>;
|
|
111
108
|
}
|
|
112
|
-
/** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number }} */
|
|
109
|
+
/** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number, maxDbConnectLatencyMs: number }} */
|
|
113
110
|
export const DEFAULT_HEALTH_CONFIG: {
|
|
114
111
|
checkIntervalMs: number;
|
|
115
112
|
staleThresholdMs: number;
|
|
116
113
|
checkTimeoutMs: number;
|
|
114
|
+
maxDbConnectLatencyMs: number;
|
|
117
115
|
};
|
|
118
116
|
import { HealthCheckCache } from "./healthCheckCache";
|
|
119
117
|
//# sourceMappingURL=healthCheckClient.d.ts.map
|
|
@@ -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":"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,4CAkCC;IAED;;;;;;OA4BC;IAED;;;;;;;;;;;;;;;;;;OA8BC;IAED,kDAeC;IAED,mCAgBC;IAED,gCAiBC;IAED;;;;;;;;;;;;;;;;;;OAIC;IAED,mBAEC;IAED,uDAiCC;IAED,yBASC;CACF;AA9TD,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"}
|
|
@@ -15,11 +15,23 @@ const {
|
|
|
15
15
|
HealthCheckCache
|
|
16
16
|
} = require('./healthCheckCache');
|
|
17
17
|
|
|
18
|
-
/**
|
|
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 }} */
|
|
19
30
|
const DEFAULT_HEALTH_CONFIG = {
|
|
20
31
|
checkIntervalMs: 30_000,
|
|
21
32
|
staleThresholdMs: 180_000,
|
|
22
|
-
checkTimeoutMs: 15_000
|
|
33
|
+
checkTimeoutMs: 15_000,
|
|
34
|
+
maxDbConnectLatencyMs: readNumberEnv('HEALTH_DB_MAX_CONNECT_LATENCY_MS') ?? 1000
|
|
23
35
|
};
|
|
24
36
|
const SENSITIVE_PATTERNS = [{
|
|
25
37
|
pattern: /(postgres(?:ql)?|mysql|mongodb|redis|amqp):\/\/([^:]+):([^@]+)@([^:/]+)(:\d+)?\/([^\s?]+)/gi,
|
|
@@ -126,14 +138,25 @@ class HealthCheckClient {
|
|
|
126
138
|
pool,
|
|
127
139
|
type
|
|
128
140
|
} = this._getPool(env, url);
|
|
129
|
-
await runHealthCheck(pool, type);
|
|
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
|
+
...timings
|
|
148
|
+
};
|
|
149
|
+
}
|
|
130
150
|
return {
|
|
131
|
-
status: 'ok'
|
|
151
|
+
status: 'ok',
|
|
152
|
+
...timings
|
|
132
153
|
};
|
|
133
154
|
} catch (err) {
|
|
155
|
+
const timings = err?.healthCheckTimings;
|
|
134
156
|
return {
|
|
135
157
|
status: 'error',
|
|
136
|
-
error: maskSensitiveData(err.message)
|
|
158
|
+
error: maskSensitiveData(err.message),
|
|
159
|
+
...(timings && typeof timings === 'object' ? timings : {})
|
|
137
160
|
};
|
|
138
161
|
}
|
|
139
162
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"healthCheckClient.js","names":["createDatabasePool","runHealthCheck","closePool","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","HealthCheckCache","DEFAULT_HEALTH_CONFIG","checkIntervalMs","staleThresholdMs","checkTimeoutMs","SENSITIVE_PATTERNS","pattern","replacement","maskSensitiveData","text","masked","replace","HealthCheckClient","constructor","options","healthConfig","config","appName","process","env","BUILD_APP_NAME","prefixLogs","_refreshPromise","_databasePools","Map","_resources","resources","redisClient","_getRedisClientForCache","_redisClientType","_cache","cacheKey","_getEnv","resource","name","redisResource","find","r","client","_getPool","url","has","pool","type","set","get","_checkDatabase","status","error","err","message","_checkRedis","pong","Promise","resolve","reject","ping","result","_performHealthCheckInternal","sortedResources","Object","keys","sort","reduce","acc","key","hasError","values","some","lastCheckAt","Date","now","isStale","_formatResult","cached","performHealthCheck","then","catch","getCachedResult","console","refreshCache","clearCache","healthHandler","req","res","json","statusCode","cleanup","clear","module","exports"],"sources":["../../src/health/healthCheckClient.js"],"sourcesContent":["const {\n createDatabasePool,\n runHealthCheck,\n closePool,\n} = require('./databaseChecker')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('../redisUtils')\nconst { HealthCheckCache } = require('./healthCheckCache')\n\n/** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number }} */\nconst DEFAULT_HEALTH_CONFIG = {\n checkIntervalMs: 30_000,\n staleThresholdMs: 180_000,\n checkTimeoutMs: 15_000,\n}\n\nconst SENSITIVE_PATTERNS = [\n {\n pattern:\n /(postgres(?:ql)?|mysql|mongodb|redis|amqp):\\/\\/([^:]+):([^@]+)@([^:/]+)(:\\d+)?\\/([^\\s?]+)/gi,\n replacement: '$1://***:***@***$5/***',\n },\n {\n pattern: /(\\w+):\\/\\/([^:]+):([^@]+)@([^\\s/]+)/gi,\n replacement: '$1://***:***@***',\n },\n {\n pattern:\n /(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)[\"\\s]*[:=][\"\\s]*([^\\s,}\"]+)/gi,\n replacement: '$1=***',\n },\n {\n pattern:\n /(database|table|schema|role|user|relation|column|index)\\s*[\"']([^\"']+)[\"']/gi,\n replacement: '$1 \"***\"',\n },\n {\n pattern: /\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d+)?\\b/g,\n replacement: '***$2',\n },\n {\n pattern: /\\b(host|hostname|server)[\"\\s]*[:=][\"\\s]*([^\\s,}\"]+)/gi,\n replacement: '$1=***',\n },\n]\n\n/**\n * @param {string} text\n * @returns {string}\n */\nfunction maskSensitiveData(text) {\n if (!text || typeof text !== 'string') return text\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 {{ env: string, url?: string } | { env: string, client?: any }} HealthResource\n */\nclass HealthCheckClient {\n /**\n * @param {Object} options\n * @param {HealthResource[]} options.resources - Must include Redis resource with client\n * @param {Object} [options.config]\n * @param {string} [options.appName] - For cache key: healthcheck:${appName}\n * @param {string} [options.cacheKey] - Redis key (overrides appName)\n */\n constructor(options = {}) {\n this.healthConfig = { ...DEFAULT_HEALTH_CONFIG, ...options.config }\n this.appName =\n options.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n\n this.prefixLogs = `[${this.appName}] [HealthCheck]`\n\n this._refreshPromise = null\n /** @type {Map<string, { pool: any, type: string }>} */\n this._databasePools = new Map()\n\n /** @type {HealthResource[]} */\n this._resources = options.resources || []\n\n const redisClient = this._getRedisClientForCache()\n if (redisClient) {\n this._redisClientType = getRedisClientType(redisClient)\n }\n\n this._cache = new HealthCheckCache({\n redisClient: redisClient || null,\n cacheKey: options.cacheKey,\n appName: this.appName,\n staleThresholdMs: this.healthConfig.staleThresholdMs,\n })\n }\n\n _getEnv(resource) {\n return resource.env ?? resource.name\n }\n\n _getRedisClientForCache() {\n const redisResource = this._resources.find(r => 'client' in r && r.client)\n return redisResource?.client || null\n }\n\n _getPool(env, url) {\n if (!this._databasePools.has(env)) {\n const { pool, type } = createDatabasePool(\n env,\n url,\n this.healthConfig.checkTimeoutMs\n )\n this._databasePools.set(env, { pool, type })\n }\n return this._databasePools.get(env)\n }\n\n async _checkDatabase(resource) {\n const env = this._getEnv(resource)\n const url = 'url' in resource ? resource.url : process.env[env]\n if (!url) {\n return { status: 'error', error: `Env ${env} not set` }\n }\n\n try {\n const { pool, type } = this._getPool(env, url)\n await runHealthCheck(pool, type)\n return { status: 'ok' }\n } catch (err) {\n return {\n status: 'error',\n error: maskSensitiveData(err.message),\n }\n }\n }\n\n async _checkRedis(resource) {\n const { client } = resource\n if (!client) return { status: 'ok' }\n\n try {\n let pong\n if (this._redisClientType === REDIS_V3) {\n pong = await new Promise((resolve, reject) => {\n client.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 client.ping()\n } else {\n return { status: 'error', error: 'Unknown Redis client type' }\n }\n\n return pong === 'PONG'\n ? { status: 'ok' }\n : { status: 'error', error: `Unexpected: ${pong}` }\n } catch (err) {\n return { status: 'error', error: maskSensitiveData(err.message) }\n }\n }\n\n async _performHealthCheckInternal() {\n const resources = {}\n\n for (const resource of this._resources) {\n const env = this._getEnv(resource)\n\n if ('client' in resource && resource.client) {\n resources[env] = await this._checkRedis(resource)\n } else {\n resources[env] = await this._checkDatabase(resource)\n }\n }\n\n const sortedResources = Object.keys(resources)\n .sort()\n .reduce((acc, key) => {\n acc[key] = resources[key]\n return acc\n }, {})\n\n const hasError = Object.values(resources).some(r => r.status === 'error')\n const lastCheckAt = Date.now()\n\n return {\n status: hasError ? 'error' : 'ok',\n lastCheckAt,\n resources: sortedResources,\n isStale: false,\n config: this.healthConfig,\n }\n }\n\n _formatResult(result, cached = false) {\n const isStale =\n !result.lastCheckAt ||\n Date.now() - result.lastCheckAt > this.healthConfig.staleThresholdMs\n\n return {\n ...result,\n isStale,\n status: isStale ? 'stale' : result.status,\n ...(isStale && {\n error:\n 'Health check data is stale, health-check worker may not be running. Resource statuses are unknown.',\n }),\n ...(cached && { cached: true }),\n }\n }\n\n async performHealthCheck() {\n if (this._refreshPromise) {\n return this._refreshPromise\n }\n\n this._refreshPromise = this._performHealthCheckInternal()\n .then(result => {\n this._refreshPromise = null\n return this._formatResult(result)\n })\n .catch(err => {\n this._refreshPromise = null\n throw err\n })\n\n return this._refreshPromise\n }\n\n async getCachedResult() {\n try {\n const cached = await this._cache.get()\n if (cached) return this._formatResult(cached)\n return null\n } catch (err) {\n console.error(`${this.prefixLogs} Failed to read from cache:`, err)\n return {\n status: 'error',\n lastCheckAt: null,\n resources: {},\n isStale: true,\n error:\n 'Redis unavailable, unable to read health status of other resources',\n config: this.healthConfig,\n }\n }\n }\n\n async refreshCache() {\n const result = await this._performHealthCheckInternal()\n await this._cache.set(result)\n return result\n }\n\n clearCache() {\n this._refreshPromise = null\n }\n\n healthHandler() {\n return async (req, res) => {\n try {\n const result = await this.getCachedResult()\n\n if (!result) {\n res.status(503).json({\n status: 'error',\n lastCheckAt: null,\n resources: {},\n isStale: true,\n error:\n 'No health check data yet, health-check worker may not be running',\n config: this.healthConfig,\n })\n return\n }\n\n const statusCode = result.status === 'ok' ? 200 : 503\n res.status(statusCode).json(result)\n } catch (err) {\n console.error(`${this.prefixLogs} Health check failed:`, err)\n res.status(503).json({\n status: 'error',\n lastCheckAt: null,\n resources: {},\n isStale: true,\n error:\n 'Redis unavailable, unable to read health status of other resources',\n config: this.healthConfig,\n })\n }\n }\n }\n\n async cleanup() {\n for (const [, { pool }] of this._databasePools) {\n try {\n await closePool(pool)\n } catch (err) {\n console.error(`${this.prefixLogs} Error closing database pool:`, err)\n }\n }\n this._databasePools.clear()\n }\n}\n\nmodule.exports = { HealthCheckClient, DEFAULT_HEALTH_CONFIG }\n"],"mappings":";;AAAA,MAAM;EACJA,kBAAkB;EAClBC,cAAc;EACdC;AACF,CAAC,GAAGC,OAAO,CAAC,mBAAmB,CAAC;AAChC,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;AACA,MAAMM,qBAAqB,GAAG;EAC5BC,eAAe,EAAE,MAAM;EACvBC,gBAAgB,EAAE,OAAO;EACzBC,cAAc,EAAE;AAClB,CAAC;AAED,MAAMC,kBAAkB,GAAG,CACzB;EACEC,OAAO,EACL,6FAA6F;EAC/FC,WAAW,EAAE;AACf,CAAC,EACD;EACED,OAAO,EAAE,uCAAuC;EAChDC,WAAW,EAAE;AACf,CAAC,EACD;EACED,OAAO,EACL,4FAA4F;EAC9FC,WAAW,EAAE;AACf,CAAC,EACD;EACED,OAAO,EACL,8EAA8E;EAChFC,WAAW,EAAE;AACf,CAAC,EACD;EACED,OAAO,EAAE,kDAAkD;EAC3DC,WAAW,EAAE;AACf,CAAC,EACD;EACED,OAAO,EAAE,uDAAuD;EAChEC,WAAW,EAAE;AACf,CAAC,CACF;;AAED;AACA;AACA;AACA;AACA,SAASC,iBAAiBA,CAACC,IAAI,EAAE;EAC/B,IAAI,CAACA,IAAI,IAAI,OAAOA,IAAI,KAAK,QAAQ,EAAE,OAAOA,IAAI;EAClD,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;AACA,MAAME,iBAAiB,CAAC;EACtB;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAACC,OAAO,GAAG,CAAC,CAAC,EAAE;IACxB,IAAI,CAACC,YAAY,GAAG;MAAE,GAAGd,qBAAqB;MAAE,GAAGa,OAAO,CAACE;IAAO,CAAC;IACnE,IAAI,CAACC,OAAO,GACVH,OAAO,CAACG,OAAO,IAAIC,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;IAEhE,IAAI,CAACC,UAAU,GAAG,IAAI,IAAI,CAACJ,OAAO,iBAAiB;IAEnD,IAAI,CAACK,eAAe,GAAG,IAAI;IAC3B;IACA,IAAI,CAACC,cAAc,GAAG,IAAIC,GAAG,CAAC,CAAC;;IAE/B;IACA,IAAI,CAACC,UAAU,GAAGX,OAAO,CAACY,SAAS,IAAI,EAAE;IAEzC,MAAMC,WAAW,GAAG,IAAI,CAACC,uBAAuB,CAAC,CAAC;IAClD,IAAID,WAAW,EAAE;MACf,IAAI,CAACE,gBAAgB,GAAGjC,kBAAkB,CAAC+B,WAAW,CAAC;IACzD;IAEA,IAAI,CAACG,MAAM,GAAG,IAAI9B,gBAAgB,CAAC;MACjC2B,WAAW,EAAEA,WAAW,IAAI,IAAI;MAChCI,QAAQ,EAAEjB,OAAO,CAACiB,QAAQ;MAC1Bd,OAAO,EAAE,IAAI,CAACA,OAAO;MACrBd,gBAAgB,EAAE,IAAI,CAACY,YAAY,CAACZ;IACtC,CAAC,CAAC;EACJ;EAEA6B,OAAOA,CAACC,QAAQ,EAAE;IAChB,OAAOA,QAAQ,CAACd,GAAG,IAAIc,QAAQ,CAACC,IAAI;EACtC;EAEAN,uBAAuBA,CAAA,EAAG;IACxB,MAAMO,aAAa,GAAG,IAAI,CAACV,UAAU,CAACW,IAAI,CAACC,CAAC,IAAI,QAAQ,IAAIA,CAAC,IAAIA,CAAC,CAACC,MAAM,CAAC;IAC1E,OAAOH,aAAa,EAAEG,MAAM,IAAI,IAAI;EACtC;EAEAC,QAAQA,CAACpB,GAAG,EAAEqB,GAAG,EAAE;IACjB,IAAI,CAAC,IAAI,CAACjB,cAAc,CAACkB,GAAG,CAACtB,GAAG,CAAC,EAAE;MACjC,MAAM;QAAEuB,IAAI;QAAEC;MAAK,CAAC,GAAGnD,kBAAkB,CACvC2B,GAAG,EACHqB,GAAG,EACH,IAAI,CAACzB,YAAY,CAACX,cACpB,CAAC;MACD,IAAI,CAACmB,cAAc,CAACqB,GAAG,CAACzB,GAAG,EAAE;QAAEuB,IAAI;QAAEC;MAAK,CAAC,CAAC;IAC9C;IACA,OAAO,IAAI,CAACpB,cAAc,CAACsB,GAAG,CAAC1B,GAAG,CAAC;EACrC;EAEA,MAAM2B,cAAcA,CAACb,QAAQ,EAAE;IAC7B,MAAMd,GAAG,GAAG,IAAI,CAACa,OAAO,CAACC,QAAQ,CAAC;IAClC,MAAMO,GAAG,GAAG,KAAK,IAAIP,QAAQ,GAAGA,QAAQ,CAACO,GAAG,GAAGtB,OAAO,CAACC,GAAG,CAACA,GAAG,CAAC;IAC/D,IAAI,CAACqB,GAAG,EAAE;MACR,OAAO;QAAEO,MAAM,EAAE,OAAO;QAAEC,KAAK,EAAE,OAAO7B,GAAG;MAAW,CAAC;IACzD;IAEA,IAAI;MACF,MAAM;QAAEuB,IAAI;QAAEC;MAAK,CAAC,GAAG,IAAI,CAACJ,QAAQ,CAACpB,GAAG,EAAEqB,GAAG,CAAC;MAC9C,MAAM/C,cAAc,CAACiD,IAAI,EAAEC,IAAI,CAAC;MAChC,OAAO;QAAEI,MAAM,EAAE;MAAK,CAAC;IACzB,CAAC,CAAC,OAAOE,GAAG,EAAE;MACZ,OAAO;QACLF,MAAM,EAAE,OAAO;QACfC,KAAK,EAAExC,iBAAiB,CAACyC,GAAG,CAACC,OAAO;MACtC,CAAC;IACH;EACF;EAEA,MAAMC,WAAWA,CAAClB,QAAQ,EAAE;IAC1B,MAAM;MAAEK;IAAO,CAAC,GAAGL,QAAQ;IAC3B,IAAI,CAACK,MAAM,EAAE,OAAO;MAAES,MAAM,EAAE;IAAK,CAAC;IAEpC,IAAI;MACF,IAAIK,IAAI;MACR,IAAI,IAAI,CAACvB,gBAAgB,KAAK9B,QAAQ,EAAE;QACtCqD,IAAI,GAAG,MAAM,IAAIC,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;UAC5CjB,MAAM,CAACkB,IAAI,CAAC,CAACP,GAAG,EAAEQ,MAAM,KAAK;YAC3B,IAAIR,GAAG,EAAEM,MAAM,CAACN,GAAG,CAAC,MACfK,OAAO,CAACG,MAAM,CAAC;UACtB,CAAC,CAAC;QACJ,CAAC,CAAC;MACJ,CAAC,MAAM,IACL,IAAI,CAAC5B,gBAAgB,KAAKhC,QAAQ,IAClC,IAAI,CAACgC,gBAAgB,KAAK/B,OAAO,EACjC;QACAsD,IAAI,GAAG,MAAMd,MAAM,CAACkB,IAAI,CAAC,CAAC;MAC5B,CAAC,MAAM;QACL,OAAO;UAAET,MAAM,EAAE,OAAO;UAAEC,KAAK,EAAE;QAA4B,CAAC;MAChE;MAEA,OAAOI,IAAI,KAAK,MAAM,GAClB;QAAEL,MAAM,EAAE;MAAK,CAAC,GAChB;QAAEA,MAAM,EAAE,OAAO;QAAEC,KAAK,EAAE,eAAeI,IAAI;MAAG,CAAC;IACvD,CAAC,CAAC,OAAOH,GAAG,EAAE;MACZ,OAAO;QAAEF,MAAM,EAAE,OAAO;QAAEC,KAAK,EAAExC,iBAAiB,CAACyC,GAAG,CAACC,OAAO;MAAE,CAAC;IACnE;EACF;EAEA,MAAMQ,2BAA2BA,CAAA,EAAG;IAClC,MAAMhC,SAAS,GAAG,CAAC,CAAC;IAEpB,KAAK,MAAMO,QAAQ,IAAI,IAAI,CAACR,UAAU,EAAE;MACtC,MAAMN,GAAG,GAAG,IAAI,CAACa,OAAO,CAACC,QAAQ,CAAC;MAElC,IAAI,QAAQ,IAAIA,QAAQ,IAAIA,QAAQ,CAACK,MAAM,EAAE;QAC3CZ,SAAS,CAACP,GAAG,CAAC,GAAG,MAAM,IAAI,CAACgC,WAAW,CAAClB,QAAQ,CAAC;MACnD,CAAC,MAAM;QACLP,SAAS,CAACP,GAAG,CAAC,GAAG,MAAM,IAAI,CAAC2B,cAAc,CAACb,QAAQ,CAAC;MACtD;IACF;IAEA,MAAM0B,eAAe,GAAGC,MAAM,CAACC,IAAI,CAACnC,SAAS,CAAC,CAC3CoC,IAAI,CAAC,CAAC,CACNC,MAAM,CAAC,CAACC,GAAG,EAAEC,GAAG,KAAK;MACpBD,GAAG,CAACC,GAAG,CAAC,GAAGvC,SAAS,CAACuC,GAAG,CAAC;MACzB,OAAOD,GAAG;IACZ,CAAC,EAAE,CAAC,CAAC,CAAC;IAER,MAAME,QAAQ,GAAGN,MAAM,CAACO,MAAM,CAACzC,SAAS,CAAC,CAAC0C,IAAI,CAAC/B,CAAC,IAAIA,CAAC,CAACU,MAAM,KAAK,OAAO,CAAC;IACzE,MAAMsB,WAAW,GAAGC,IAAI,CAACC,GAAG,CAAC,CAAC;IAE9B,OAAO;MACLxB,MAAM,EAAEmB,QAAQ,GAAG,OAAO,GAAG,IAAI;MACjCG,WAAW;MACX3C,SAAS,EAAEiC,eAAe;MAC1Ba,OAAO,EAAE,KAAK;MACdxD,MAAM,EAAE,IAAI,CAACD;IACf,CAAC;EACH;EAEA0D,aAAaA,CAAChB,MAAM,EAAEiB,MAAM,GAAG,KAAK,EAAE;IACpC,MAAMF,OAAO,GACX,CAACf,MAAM,CAACY,WAAW,IACnBC,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGd,MAAM,CAACY,WAAW,GAAG,IAAI,CAACtD,YAAY,CAACZ,gBAAgB;IAEtE,OAAO;MACL,GAAGsD,MAAM;MACTe,OAAO;MACPzB,MAAM,EAAEyB,OAAO,GAAG,OAAO,GAAGf,MAAM,CAACV,MAAM;MACzC,IAAIyB,OAAO,IAAI;QACbxB,KAAK,EACH;MACJ,CAAC,CAAC;MACF,IAAI0B,MAAM,IAAI;QAAEA,MAAM,EAAE;MAAK,CAAC;IAChC,CAAC;EACH;EAEA,MAAMC,kBAAkBA,CAAA,EAAG;IACzB,IAAI,IAAI,CAACrD,eAAe,EAAE;MACxB,OAAO,IAAI,CAACA,eAAe;IAC7B;IAEA,IAAI,CAACA,eAAe,GAAG,IAAI,CAACoC,2BAA2B,CAAC,CAAC,CACtDkB,IAAI,CAACnB,MAAM,IAAI;MACd,IAAI,CAACnC,eAAe,GAAG,IAAI;MAC3B,OAAO,IAAI,CAACmD,aAAa,CAAChB,MAAM,CAAC;IACnC,CAAC,CAAC,CACDoB,KAAK,CAAC5B,GAAG,IAAI;MACZ,IAAI,CAAC3B,eAAe,GAAG,IAAI;MAC3B,MAAM2B,GAAG;IACX,CAAC,CAAC;IAEJ,OAAO,IAAI,CAAC3B,eAAe;EAC7B;EAEA,MAAMwD,eAAeA,CAAA,EAAG;IACtB,IAAI;MACF,MAAMJ,MAAM,GAAG,MAAM,IAAI,CAAC5C,MAAM,CAACe,GAAG,CAAC,CAAC;MACtC,IAAI6B,MAAM,EAAE,OAAO,IAAI,CAACD,aAAa,CAACC,MAAM,CAAC;MAC7C,OAAO,IAAI;IACb,CAAC,CAAC,OAAOzB,GAAG,EAAE;MACZ8B,OAAO,CAAC/B,KAAK,CAAC,GAAG,IAAI,CAAC3B,UAAU,6BAA6B,EAAE4B,GAAG,CAAC;MACnE,OAAO;QACLF,MAAM,EAAE,OAAO;QACfsB,WAAW,EAAE,IAAI;QACjB3C,SAAS,EAAE,CAAC,CAAC;QACb8C,OAAO,EAAE,IAAI;QACbxB,KAAK,EACH,oEAAoE;QACtEhC,MAAM,EAAE,IAAI,CAACD;MACf,CAAC;IACH;EACF;EAEA,MAAMiE,YAAYA,CAAA,EAAG;IACnB,MAAMvB,MAAM,GAAG,MAAM,IAAI,CAACC,2BAA2B,CAAC,CAAC;IACvD,MAAM,IAAI,CAAC5B,MAAM,CAACc,GAAG,CAACa,MAAM,CAAC;IAC7B,OAAOA,MAAM;EACf;EAEAwB,UAAUA,CAAA,EAAG;IACX,IAAI,CAAC3D,eAAe,GAAG,IAAI;EAC7B;EAEA4D,aAAaA,CAAA,EAAG;IACd,OAAO,OAAOC,GAAG,EAAEC,GAAG,KAAK;MACzB,IAAI;QACF,MAAM3B,MAAM,GAAG,MAAM,IAAI,CAACqB,eAAe,CAAC,CAAC;QAE3C,IAAI,CAACrB,MAAM,EAAE;UACX2B,GAAG,CAACrC,MAAM,CAAC,GAAG,CAAC,CAACsC,IAAI,CAAC;YACnBtC,MAAM,EAAE,OAAO;YACfsB,WAAW,EAAE,IAAI;YACjB3C,SAAS,EAAE,CAAC,CAAC;YACb8C,OAAO,EAAE,IAAI;YACbxB,KAAK,EACH,kEAAkE;YACpEhC,MAAM,EAAE,IAAI,CAACD;UACf,CAAC,CAAC;UACF;QACF;QAEA,MAAMuE,UAAU,GAAG7B,MAAM,CAACV,MAAM,KAAK,IAAI,GAAG,GAAG,GAAG,GAAG;QACrDqC,GAAG,CAACrC,MAAM,CAACuC,UAAU,CAAC,CAACD,IAAI,CAAC5B,MAAM,CAAC;MACrC,CAAC,CAAC,OAAOR,GAAG,EAAE;QACZ8B,OAAO,CAAC/B,KAAK,CAAC,GAAG,IAAI,CAAC3B,UAAU,uBAAuB,EAAE4B,GAAG,CAAC;QAC7DmC,GAAG,CAACrC,MAAM,CAAC,GAAG,CAAC,CAACsC,IAAI,CAAC;UACnBtC,MAAM,EAAE,OAAO;UACfsB,WAAW,EAAE,IAAI;UACjB3C,SAAS,EAAE,CAAC,CAAC;UACb8C,OAAO,EAAE,IAAI;UACbxB,KAAK,EACH,oEAAoE;UACtEhC,MAAM,EAAE,IAAI,CAACD;QACf,CAAC,CAAC;MACJ;IACF,CAAC;EACH;EAEA,MAAMwE,OAAOA,CAAA,EAAG;IACd,KAAK,MAAM,GAAG;MAAE7C;IAAK,CAAC,CAAC,IAAI,IAAI,CAACnB,cAAc,EAAE;MAC9C,IAAI;QACF,MAAM7B,SAAS,CAACgD,IAAI,CAAC;MACvB,CAAC,CAAC,OAAOO,GAAG,EAAE;QACZ8B,OAAO,CAAC/B,KAAK,CAAC,GAAG,IAAI,CAAC3B,UAAU,+BAA+B,EAAE4B,GAAG,CAAC;MACvE;IACF;IACA,IAAI,CAAC1B,cAAc,CAACiE,KAAK,CAAC,CAAC;EAC7B;AACF;AAEAC,MAAM,CAACC,OAAO,GAAG;EAAE9E,iBAAiB;EAAEX;AAAsB,CAAC","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"healthCheckClient.js","names":["createDatabasePool","runHealthCheck","closePool","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","HealthCheckCache","readNumberEnv","name","raw","process","env","undefined","num","Number","isFinite","DEFAULT_HEALTH_CONFIG","checkIntervalMs","staleThresholdMs","checkTimeoutMs","maxDbConnectLatencyMs","SENSITIVE_PATTERNS","pattern","replacement","maskSensitiveData","text","masked","replace","HealthCheckClient","constructor","options","healthConfig","config","appName","BUILD_APP_NAME","prefixLogs","_refreshPromise","_databasePools","Map","_resources","resources","redisClient","_getRedisClientForCache","_redisClientType","_cache","cacheKey","_getEnv","resource","redisResource","find","r","client","_getPool","url","has","pool","type","set","get","_checkDatabase","status","error","timings","maxConnect","connectMs","err","healthCheckTimings","message","_checkRedis","pong","Promise","resolve","reject","ping","result","_performHealthCheckInternal","sortedResources","Object","keys","sort","reduce","acc","key","hasError","values","some","lastCheckAt","Date","now","isStale","_formatResult","cached","performHealthCheck","then","catch","getCachedResult","console","refreshCache","clearCache","healthHandler","req","res","json","statusCode","cleanup","clear","module","exports"],"sources":["../../src/health/healthCheckClient.js"],"sourcesContent":["const {\n createDatabasePool,\n runHealthCheck,\n closePool,\n} = require('./databaseChecker')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('../redisUtils')\nconst { HealthCheckCache } = require('./healthCheckCache')\n\n/**\n * @param {string} name\n * @returns {number | undefined}\n */\nfunction readNumberEnv(name) {\n const raw = process.env[name]\n if (raw == null || raw === '') return undefined\n const num = Number(raw)\n return Number.isFinite(num) ? num : undefined\n}\n\n/** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number, maxDbConnectLatencyMs: number }} */\nconst DEFAULT_HEALTH_CONFIG = {\n checkIntervalMs: 30_000,\n staleThresholdMs: 180_000,\n checkTimeoutMs: 15_000,\n maxDbConnectLatencyMs:\n readNumberEnv('HEALTH_DB_MAX_CONNECT_LATENCY_MS') ?? 1000,\n}\n\nconst SENSITIVE_PATTERNS = [\n {\n pattern:\n /(postgres(?:ql)?|mysql|mongodb|redis|amqp):\\/\\/([^:]+):([^@]+)@([^:/]+)(:\\d+)?\\/([^\\s?]+)/gi,\n replacement: '$1://***:***@***$5/***',\n },\n {\n pattern: /(\\w+):\\/\\/([^:]+):([^@]+)@([^\\s/]+)/gi,\n replacement: '$1://***:***@***',\n },\n {\n pattern:\n /(password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?token)[\"\\s]*[:=][\"\\s]*([^\\s,}\"]+)/gi,\n replacement: '$1=***',\n },\n {\n pattern:\n /(database|table|schema|role|user|relation|column|index)\\s*[\"']([^\"']+)[\"']/gi,\n replacement: '$1 \"***\"',\n },\n {\n pattern: /\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d+)?\\b/g,\n replacement: '***$2',\n },\n {\n pattern: /\\b(host|hostname|server)[\"\\s]*[:=][\"\\s]*([^\\s,}\"]+)/gi,\n replacement: '$1=***',\n },\n]\n\n/**\n * @param {string} text\n * @returns {string}\n */\nfunction maskSensitiveData(text) {\n if (!text || typeof text !== 'string') return text\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 {{ env: string, url?: string } | { env: string, client?: any }} HealthResource\n */\nclass HealthCheckClient {\n /**\n * @param {Object} options\n * @param {HealthResource[]} options.resources - Must include Redis resource with client\n * @param {Object} [options.config]\n * @param {string} [options.appName] - For cache key: healthcheck:${appName}\n * @param {string} [options.cacheKey] - Redis key (overrides appName)\n */\n constructor(options = {}) {\n this.healthConfig = { ...DEFAULT_HEALTH_CONFIG, ...options.config }\n this.appName =\n options.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n\n this.prefixLogs = `[${this.appName}] [HealthCheck]`\n\n this._refreshPromise = null\n /** @type {Map<string, { pool: any, type: string }>} */\n this._databasePools = new Map()\n\n /** @type {HealthResource[]} */\n this._resources = options.resources || []\n\n const redisClient = this._getRedisClientForCache()\n if (redisClient) {\n this._redisClientType = getRedisClientType(redisClient)\n }\n\n this._cache = new HealthCheckCache({\n redisClient: redisClient || null,\n cacheKey: options.cacheKey,\n appName: this.appName,\n staleThresholdMs: this.healthConfig.staleThresholdMs,\n })\n }\n\n _getEnv(resource) {\n return resource.env ?? resource.name\n }\n\n _getRedisClientForCache() {\n const redisResource = this._resources.find(r => 'client' in r && r.client)\n return redisResource?.client || null\n }\n\n _getPool(env, url) {\n if (!this._databasePools.has(env)) {\n const { pool, type } = createDatabasePool(\n env,\n url,\n this.healthConfig.checkTimeoutMs\n )\n this._databasePools.set(env, { pool, type })\n }\n return this._databasePools.get(env)\n }\n\n async _checkDatabase(resource) {\n const env = this._getEnv(resource)\n const url = 'url' in resource ? resource.url : process.env[env]\n if (!url) {\n return { status: 'error', error: `Env ${env} not set` }\n }\n\n try {\n const { pool, type } = this._getPool(env, url)\n const timings = await runHealthCheck(pool, type)\n\n const maxConnect = this.healthConfig.maxDbConnectLatencyMs\n if (\n typeof maxConnect === 'number' &&\n Number.isFinite(maxConnect) &&\n timings?.connectMs != null &&\n timings.connectMs > maxConnect\n ) {\n return {\n status: 'error',\n error: `DB connect latency ${timings.connectMs}ms exceeds ${maxConnect}ms`,\n ...timings,\n }\n }\n\n return { status: 'ok', ...timings }\n } catch (err) {\n const timings = err?.healthCheckTimings\n return {\n status: 'error',\n error: maskSensitiveData(err.message),\n ...(timings && typeof timings === 'object' ? timings : {}),\n }\n }\n }\n\n async _checkRedis(resource) {\n const { client } = resource\n if (!client) return { status: 'ok' }\n\n try {\n let pong\n if (this._redisClientType === REDIS_V3) {\n pong = await new Promise((resolve, reject) => {\n client.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 client.ping()\n } else {\n return { status: 'error', error: 'Unknown Redis client type' }\n }\n\n return pong === 'PONG'\n ? { status: 'ok' }\n : { status: 'error', error: `Unexpected: ${pong}` }\n } catch (err) {\n return { status: 'error', error: maskSensitiveData(err.message) }\n }\n }\n\n async _performHealthCheckInternal() {\n const resources = {}\n\n for (const resource of this._resources) {\n const env = this._getEnv(resource)\n\n if ('client' in resource && resource.client) {\n resources[env] = await this._checkRedis(resource)\n } else {\n resources[env] = await this._checkDatabase(resource)\n }\n }\n\n const sortedResources = Object.keys(resources)\n .sort()\n .reduce((acc, key) => {\n acc[key] = resources[key]\n return acc\n }, {})\n\n const hasError = Object.values(resources).some(r => r.status === 'error')\n const lastCheckAt = Date.now()\n\n return {\n status: hasError ? 'error' : 'ok',\n lastCheckAt,\n resources: sortedResources,\n isStale: false,\n config: this.healthConfig,\n }\n }\n\n _formatResult(result, cached = false) {\n const isStale =\n !result.lastCheckAt ||\n Date.now() - result.lastCheckAt > this.healthConfig.staleThresholdMs\n\n return {\n ...result,\n isStale,\n status: isStale ? 'stale' : result.status,\n ...(isStale && {\n error:\n 'Health check data is stale, health-check worker may not be running. Resource statuses are unknown.',\n }),\n ...(cached && { cached: true }),\n }\n }\n\n async performHealthCheck() {\n if (this._refreshPromise) {\n return this._refreshPromise\n }\n\n this._refreshPromise = this._performHealthCheckInternal()\n .then(result => {\n this._refreshPromise = null\n return this._formatResult(result)\n })\n .catch(err => {\n this._refreshPromise = null\n throw err\n })\n\n return this._refreshPromise\n }\n\n async getCachedResult() {\n try {\n const cached = await this._cache.get()\n if (cached) return this._formatResult(cached)\n return null\n } catch (err) {\n console.error(`${this.prefixLogs} Failed to read from cache:`, err)\n return {\n status: 'error',\n lastCheckAt: null,\n resources: {},\n isStale: true,\n error:\n 'Redis unavailable, unable to read health status of other resources',\n config: this.healthConfig,\n }\n }\n }\n\n async refreshCache() {\n const result = await this._performHealthCheckInternal()\n await this._cache.set(result)\n return result\n }\n\n clearCache() {\n this._refreshPromise = null\n }\n\n healthHandler() {\n return async (req, res) => {\n try {\n const result = await this.getCachedResult()\n\n if (!result) {\n res.status(503).json({\n status: 'error',\n lastCheckAt: null,\n resources: {},\n isStale: true,\n error:\n 'No health check data yet, health-check worker may not be running',\n config: this.healthConfig,\n })\n return\n }\n\n const statusCode = result.status === 'ok' ? 200 : 503\n res.status(statusCode).json(result)\n } catch (err) {\n console.error(`${this.prefixLogs} Health check failed:`, err)\n res.status(503).json({\n status: 'error',\n lastCheckAt: null,\n resources: {},\n isStale: true,\n error:\n 'Redis unavailable, unable to read health status of other resources',\n config: this.healthConfig,\n })\n }\n }\n }\n\n async cleanup() {\n for (const [, { pool }] of this._databasePools) {\n try {\n await closePool(pool)\n } catch (err) {\n console.error(`${this.prefixLogs} Error closing database pool:`, err)\n }\n }\n this._databasePools.clear()\n }\n}\n\nmodule.exports = { HealthCheckClient, DEFAULT_HEALTH_CONFIG }\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -4,6 +4,13 @@ const mysql = require('mysql2/promise')
|
|
|
4
4
|
const DB_TYPE_POSTGRES = 'postgres'
|
|
5
5
|
const DB_TYPE_MYSQL = 'mysql'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
function nowMs() {
|
|
11
|
+
return Number(process.hrtime.bigint() / 1_000_000n)
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* @param {string} url
|
|
9
16
|
* @returns {string} postgres | mysql
|
|
@@ -81,14 +88,78 @@ function createDatabasePool(env, url, connectionTimeoutMs) {
|
|
|
81
88
|
/**
|
|
82
89
|
* @param {any} pool
|
|
83
90
|
* @param {string} type
|
|
84
|
-
* @returns {Promise<
|
|
91
|
+
* @returns {Promise<{ connectMs: number, queryMs: number, totalMs: number }>}
|
|
85
92
|
*/
|
|
86
93
|
async function runHealthCheck(pool, type) {
|
|
87
94
|
if (type === DB_TYPE_MYSQL) {
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
const startTotal = nowMs()
|
|
96
|
+
let connectMs = 0
|
|
97
|
+
let queryMs = 0
|
|
98
|
+
|
|
99
|
+
/** @type {any} */
|
|
100
|
+
let conn
|
|
101
|
+
try {
|
|
102
|
+
const startConnect = nowMs()
|
|
103
|
+
conn = await pool.getConnection()
|
|
104
|
+
connectMs = nowMs() - startConnect
|
|
105
|
+
|
|
106
|
+
const startQuery = nowMs()
|
|
107
|
+
await conn.query('SELECT 1')
|
|
108
|
+
queryMs = nowMs() - startQuery
|
|
109
|
+
|
|
110
|
+
return { connectMs, queryMs, totalMs: nowMs() - startTotal }
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const totalMs = nowMs() - startTotal
|
|
113
|
+
err.healthCheckTimings = {
|
|
114
|
+
connectMs,
|
|
115
|
+
queryMs,
|
|
116
|
+
totalMs,
|
|
117
|
+
}
|
|
118
|
+
throw err
|
|
119
|
+
} finally {
|
|
120
|
+
if (conn) {
|
|
121
|
+
try {
|
|
122
|
+
conn.release()
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const startTotal = nowMs()
|
|
131
|
+
let connectMs = 0
|
|
132
|
+
let queryMs = 0
|
|
133
|
+
|
|
134
|
+
/** @type {import('pg').PoolClient | null} */
|
|
135
|
+
let client = null
|
|
136
|
+
try {
|
|
137
|
+
const startConnect = nowMs()
|
|
138
|
+
client = await pool.connect()
|
|
139
|
+
connectMs = nowMs() - startConnect
|
|
140
|
+
|
|
141
|
+
const startQuery = nowMs()
|
|
142
|
+
await client.query('SELECT 1')
|
|
143
|
+
queryMs = nowMs() - startQuery
|
|
144
|
+
|
|
145
|
+
return { connectMs, queryMs, totalMs: nowMs() - startTotal }
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const totalMs = nowMs() - startTotal
|
|
148
|
+
err.healthCheckTimings = {
|
|
149
|
+
connectMs,
|
|
150
|
+
queryMs,
|
|
151
|
+
totalMs,
|
|
152
|
+
}
|
|
153
|
+
throw err
|
|
154
|
+
} finally {
|
|
155
|
+
if (client) {
|
|
156
|
+
try {
|
|
157
|
+
client.release()
|
|
158
|
+
} catch {
|
|
159
|
+
// ignore
|
|
160
|
+
}
|
|
161
|
+
}
|
|
90
162
|
}
|
|
91
|
-
await pool.query('SELECT 1')
|
|
92
163
|
}
|
|
93
164
|
|
|
94
165
|
/**
|
|
@@ -11,11 +11,24 @@ const {
|
|
|
11
11
|
} = require('../redisUtils')
|
|
12
12
|
const { HealthCheckCache } = require('./healthCheckCache')
|
|
13
13
|
|
|
14
|
-
/**
|
|
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 }} */
|
|
15
26
|
const DEFAULT_HEALTH_CONFIG = {
|
|
16
27
|
checkIntervalMs: 30_000,
|
|
17
28
|
staleThresholdMs: 180_000,
|
|
18
29
|
checkTimeoutMs: 15_000,
|
|
30
|
+
maxDbConnectLatencyMs:
|
|
31
|
+
readNumberEnv('HEALTH_DB_MAX_CONNECT_LATENCY_MS') ?? 1000,
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
const SENSITIVE_PATTERNS = [
|
|
@@ -129,12 +142,29 @@ class HealthCheckClient {
|
|
|
129
142
|
|
|
130
143
|
try {
|
|
131
144
|
const { pool, type } = this._getPool(env, url)
|
|
132
|
-
await runHealthCheck(pool, type)
|
|
133
|
-
|
|
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
|
+
...timings,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { status: 'ok', ...timings }
|
|
134
162
|
} catch (err) {
|
|
163
|
+
const timings = err?.healthCheckTimings
|
|
135
164
|
return {
|
|
136
165
|
status: 'error',
|
|
137
166
|
error: maskSensitiveData(err.message),
|
|
167
|
+
...(timings && typeof timings === 'object' ? timings : {}),
|
|
138
168
|
}
|
|
139
169
|
}
|
|
140
170
|
}
|