@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 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<void>}
31
+ * @returns {Promise<{ connectMs: number, queryMs: number, totalMs: number }>}
32
32
  */
33
- export function runHealthCheck(pool: any, type: string): Promise<void>;
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":"AAMA;;;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,GACJ,QAAQ,IAAI,CAAC,CAQzB;AAED;;;GAGG;AACH,gCAHW,GAAG,GACD,QAAQ,IAAI,CAAC,CAQzB;AApGD,0CAAmC;AACnC,oCAA6B"}
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<void>}
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
- await pool.execute('SELECT 1');
96
- return;
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","execute","query","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 * @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<void>}\n */\nasync function runHealthCheck(pool, type) {\n if (type === DB_TYPE_MYSQL) {\n await pool.execute('SELECT 1')\n return\n }\n await pool.query('SELECT 1')\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":[]}
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":"6BAgEa;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;;;;;;OAiBC;IAED;;;;;;OA4BC;IAED;;;;;;;;;;;;;;;;;OA8BC;IAED,kDAeC;IAED,mCAgBC;IAED,gCAiBC;IAED;;;;;;;;;;;;;;;;;OAIC;IAED,mBAEC;IAED,uDAiCC;IAED,yBASC;CACF;AA3SD,4FAA4F;AAC5F,oCADW;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAKvF"}
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
- /** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number }} */
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":"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","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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adalo/metrics",
3
- "version": "0.1.149",
3
+ "version": "0.1.150",
4
4
  "description": "Reusable metrics utilities for Node.js and Laravel apps",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -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<void>}
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
- await pool.execute('SELECT 1')
89
- return
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
- /** @type {{ checkIntervalMs: number, staleThresholdMs: number, checkTimeoutMs: number }} */
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
- return { status: 'ok' }
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
  }