@adalo/metrics 0.1.148 → 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":[]}
@@ -44,6 +44,8 @@ export class RedisMetricsClient extends BaseMetricsClient {
44
44
  redisMemoryGauge: import("prom-client").Gauge<string>;
45
45
  /** Gauge for Redis operation stats */
46
46
  redisStatsGauge: import("prom-client").Gauge<string>;
47
+ _redisConnSeenKeys: Set<any>;
48
+ _redisConnZeroedKeys: Set<any>;
47
49
  getRedisConnections: () => Promise<any>;
48
50
  getRedisInfo: (section: any) => Promise<any>;
49
51
  parseRedisConnections: (clientsStr: any) => any;
@@ -1 +1 @@
1
- {"version":3,"file":"metricsRedisClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsRedisClient.js"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH;IACE;;;;;;;;;;;;;;OAcG;IACH;QAbwB,WAAW,EAAxB,GAAG;QACc,OAAO;QACP,MAAM;QACN,WAAW;QACV,OAAO;QACP,SAAS;QACV,cAAc;QACd,iBAAiB;QACjB,WAAW;QACV,gBAAgB;QACf,iBAAiB;QAClB,kBAAkB;OA8C9C;IAhCC,oCAAoC;IACpC,iBAA8B;IAC9B,wBAAsD;IAEtD,2CAA2C;IAC3C,2DAIE;IAEF,iEAIE;IAEF,mCAAmC;IACnC,sDAIE;IAEF,sCAAsC;IACtC,qDAIE;IAKJ,wCAgCC;IAED,6CAuBC;IAED,gDAkBC;IAED;;;OAGG;IACH,2BAFa,QAAQ,IAAI,CAAC,CA8HzB;IAED;;;OAGG;IACH,wBAFa,QAAQ,IAAI,CAAC,CAqBzB;IAED;;;OAGG;IACH,sDAMC;CA4BF"}
1
+ {"version":3,"file":"metricsRedisClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsRedisClient.js"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH;IACE;;;;;;;;;;;;;;OAcG;IACH;QAbwB,WAAW,EAAxB,GAAG;QACc,OAAO;QACP,MAAM;QACN,WAAW;QACV,OAAO;QACP,SAAS;QACV,cAAc;QACd,iBAAiB;QACjB,WAAW;QACV,gBAAgB;QACf,iBAAiB;QAClB,kBAAkB;OAuD9C;IAzCC,oCAAoC;IACpC,iBAA8B;IAC9B,wBAAsD;IAEtD,2CAA2C;IAC3C,2DAIE;IAEF,iEAIE;IAEF,mCAAmC;IACnC,sDAIE;IAEF,sCAAsC;IACtC,qDAIE;IAQF,6BAAmC;IACnC,+BAAqC;IAKvC,wCAgCC;IAED,6CAuBC;IAED,gDAkBC;IAED;;;OAGG;IACH,2BAFa,QAAQ,IAAI,CAAC,CAkKzB;IAED;;;OAGG;IACH,wBAFa,QAAQ,IAAI,CAAC,CAqBzB;IAED;;;OAGG;IACH,sDAMC;CA4BF"}
@@ -74,6 +74,15 @@ class RedisMetricsClient extends BaseMetricsClient {
74
74
  help: 'Redis operation statistics',
75
75
  labelNames: this.withDefaultLabels(['operation'])
76
76
  });
77
+
78
+ // Track emitted connection label combinations so we can:
79
+ // - emit a one-shot 0 for series that disappear (overwrite last non-zero sample)
80
+ // - then stop emitting them on subsequent scrapes
81
+ //
82
+ // This is mainly needed because we label connections by `cmd`,
83
+ // and Redis `CLIENT LIST` `cmd` is volatile.
84
+ this._redisConnSeenKeys = new Set();
85
+ this._redisConnZeroedKeys = new Set();
77
86
  this._setCleanupHandlers();
78
87
  }
79
88
  getRedisConnections = async () => {
@@ -155,8 +164,6 @@ class RedisMetricsClient extends BaseMetricsClient {
155
164
  const [memoryInfoStr, statsInfoStr, connectionsInfoStr] = await Promise.all([this.getRedisInfo('memory'), this.getRedisInfo('stats'), this.getRedisConnections()]);
156
165
  const labels = this.getDefaultLabels();
157
166
  const connections = this.parseRedisConnections(connectionsInfoStr);
158
- this.redisConnectionsGauge.reset();
159
- this.redisConnectionsMemoryGauge.reset();
160
167
  if (this.logValues) {
161
168
  // "IN" data: raw client list from Redis
162
169
  console.log('[queue-metrics] Redis CLIENT LIST (raw):\n', connectionsInfoStr);
@@ -206,13 +213,51 @@ class RedisMetricsClient extends BaseMetricsClient {
206
213
  };
207
214
  }
208
215
  grouped[key].count += 1;
209
- grouped[key].memory += parseInt(totMem, 10);
216
+ const mem = parseInt(totMem, 10);
217
+ grouped[key].memory += Number.isFinite(mem) ? mem : 0;
210
218
  });
219
+
220
+ // Ensure sets exist even if older instance is hot-reloaded.
221
+ if (!this._redisConnSeenKeys) this._redisConnSeenKeys = new Set();
222
+ if (!this._redisConnZeroedKeys) this._redisConnZeroedKeys = new Set();
223
+ const currentKeys = new Set(Object.keys(grouped));
224
+ for (const k of currentKeys) {
225
+ this._redisConnSeenKeys.add(k);
226
+ if (this._redisConnZeroedKeys.has(k)) this._redisConnZeroedKeys.delete(k);
227
+ }
228
+
229
+ // Reset gauges each scrape so we only export:
230
+ // - current snapshot series
231
+ // - one-shot zeros for recently disappeared series
232
+ this.redisConnectionsGauge.reset();
233
+ this.redisConnectionsMemoryGauge.reset();
234
+
235
+ // Emit one-shot zeros for series that disappeared since last seen.
236
+ for (const k of this._redisConnSeenKeys) {
237
+ if (currentKeys.has(k)) continue;
238
+ if (this._redisConnZeroedKeys.has(k)) continue;
239
+ try {
240
+ const valueLabels = JSON.parse(k);
241
+ this.redisConnectionsGauge.set({
242
+ ...labels,
243
+ ...valueLabels
244
+ }, 0);
245
+ this.redisConnectionsMemoryGauge.set({
246
+ ...labels,
247
+ ...valueLabels
248
+ }, 0);
249
+ this._redisConnZeroedKeys.add(k);
250
+ } catch {
251
+ // ignore malformed key
252
+ }
253
+ }
211
254
  if (this.logValues) {
212
255
  const groups = Object.values(grouped);
213
256
  const groupedTotal = groups.reduce((sum, g) => sum + (g.count || 0), 0);
214
257
  console.log('[queue-metrics] Redis connections grouped buckets:', groups.length);
215
258
  console.log('[queue-metrics] Redis connections grouped total:', groupedTotal);
259
+ console.log('[queue-metrics] Redis connections seen keys:', this._redisConnSeenKeys.size);
260
+ console.log('[queue-metrics] Redis connections zeroed keys:', this._redisConnZeroedKeys.size);
216
261
  console.log('[queue-metrics] Redis connections grouped sample (first 10):', JSON.stringify(groups.slice(0, 10), null, 2));
217
262
  }
218
263
  Object.values(grouped).forEach(({
@@ -1 +1 @@
1
- {"version":3,"file":"metricsRedisClient.js","names":["BaseMetricsClient","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","redisConnectionStableFields","redisConnectionFields","RedisMetricsClient","constructor","redisClient","metricsConfig","intervalSec","parseInt","process","env","METRICS_QUEUE_INTERVAL_SEC","processType","redisClientType","redisConnectionsGauge","createGauge","name","help","labelNames","withDefaultLabels","redisConnectionsMemoryGauge","redisMemoryGauge","redisStatsGauge","_setCleanupHandlers","getRedisConnections","Error","Promise","resolve","reject","send_command","err","result","message","sendCommand","client","getRedisInfo","section","info","parseRedisConnections","clientsStr","split","filter","line","trim","map","parts","forEach","p","eqIdx","indexOf","k","slice","v","includes","collectRedisMetrics","memoryInfoStr","statsInfoStr","connectionsInfoStr","all","labels","getDefaultLabels","connections","reset","logValues","console","log","length","JSON","stringify","missing","flags","cmd","c","grouped","conn","totMem","key","count","memory","groups","Object","values","groupedTotal","reduce","sum","g","valueLabels","set","parseRedisInfo","infoStr","fromEntries","startsWith","stats","used_memory","memory_type","maxmemory","instantaneous_ops_per_sec","operation","error","warn","pushRedisMetrics","gatewayPush","clearAllCounters","metricsLogValues","metricObjects","registry","getMetricsAsJSON","startPush","_startPush","catch","cleanup","quit","disconnect","exit","on","module","exports"],"sources":["../../src/metrics/metricsRedisClient.js"],"sourcesContent":["const { BaseMetricsClient } = require('./baseMetricsClient')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('../redisUtils')\n\nconst redisConnectionStableFields = ['name', 'flags', 'cmd']\nconst redisConnectionFields = ['name', 'flags', 'tot-mem', 'cmd']\n\n/**\n * RedisMetricsClient extends BaseMetricsClient to collect\n * Redis metrics periodically and push them to Prometheus Pushgateway.\n *\n * @extends BaseMetricsClient\n */\nclass RedisMetricsClient extends BaseMetricsClient {\n /**\n * @param {Object} options\n * @param {any} options.redisClient - Redis client instance (required)\n * @param {string} [options.appName] - Application name (from BaseMetricsClient)\n * @param {string} [options.dynoId] - Dyno/instance ID (from BaseMetricsClient)\n * @param {string} [options.processType] - Process type (from BaseMetricsClient)\n * @param {boolean} [options.enabled] - Enable metrics collection (from BaseMetricsClient)\n * @param {boolean} [options.logValues] - Log metrics values (from BaseMetricsClient)\n * @param {string} [options.pushgatewayUrl] - PushGateway URL (from BaseMetricsClient)\n * @param {string} [options.pushgatewaySecret] - PushGateway secret token (from BaseMetricsClient)\n * @param {number} [options.intervalSec] - Interval in seconds for pushing metrics (from BaseMetricsClient)\n * @param {boolean} [options.removeOldMetrics] - Remove old metrics by service (from BaseMetricsClient)\n * @param {function} [options.startupValidation] - Function to validate startup (from BaseMetricsClient)\n * @param {boolean} [options.disablePushgateway] - Disable pushing to Pushgateway (use HTTP scraping instead)\n */\n constructor({ redisClient, ...metricsConfig } = {}) {\n const intervalSec =\n metricsConfig.intervalSec ||\n parseInt(process.env.METRICS_QUEUE_INTERVAL_SEC || '', 10) ||\n 5\n\n super({\n ...metricsConfig,\n processType: metricsConfig.processType || 'queue-metrics',\n intervalSec,\n })\n\n /** Redis client used for metrics */\n this.redisClient = redisClient\n this.redisClientType = getRedisClientType(redisClient)\n\n /** Counter for Redis client connections */\n this.redisConnectionsGauge = this.createGauge({\n name: 'app_redis_connections_count',\n help: 'Redis client connections',\n labelNames: this.withDefaultLabels(redisConnectionStableFields),\n })\n\n this.redisConnectionsMemoryGauge = this.createGauge({\n name: 'app_redis_connections_memory_usage_count',\n help: 'Redis client connections',\n labelNames: this.withDefaultLabels(redisConnectionStableFields),\n })\n\n /** Gauge for Redis memory usage */\n this.redisMemoryGauge = this.createGauge({\n name: 'app_redis_memory_bytes',\n help: 'Redis memory usage in bytes',\n labelNames: this.withDefaultLabels(['memory_type']),\n })\n\n /** Gauge for Redis operation stats */\n this.redisStatsGauge = this.createGauge({\n name: 'app_redis_stats_total',\n help: 'Redis operation statistics',\n labelNames: this.withDefaultLabels(['operation']),\n })\n\n this._setCleanupHandlers()\n }\n\n getRedisConnections = async () => {\n if (!this.redisClient) throw new Error('Redis client not provided')\n\n // node-redis v3 (uses callback)\n if (this.redisClientType === REDIS_V3) {\n return new Promise((resolve, reject) => {\n this.redisClient.send_command('CLIENT', ['LIST'], (err, result) => {\n if (err) {\n reject(new Error(`Failed to get CLIENT LIST: ${err.message}`))\n } else resolve(result)\n })\n })\n }\n\n // node-redis v4\n if (this.redisClientType === REDIS_V4) {\n try {\n return this.redisClient.sendCommand(['CLIENT', 'LIST'])\n } catch (err) {\n throw new Error(`Failed to get CLIENT LIST: ${err.message}`)\n }\n }\n\n if (this.redisClientType === IOREDIS) {\n try {\n return this.redisClient.client('LIST')\n } catch (err) {\n throw new Error(`Failed to get CLIENT LIST: ${err.message}`)\n }\n }\n\n throw new Error('Unsupported Redis client type')\n }\n\n getRedisInfo = async section => {\n if (!this.redisClient) throw new Error('Redis client not provided')\n\n // node-redis v3 (uses callback)\n if (this.redisClientType === REDIS_V3) {\n return new Promise((resolve, reject) => {\n this.redisClient.info(section, (err, result) => {\n if (err) reject(err)\n else resolve(result)\n })\n })\n }\n\n // node-redis v4 or ioredis (info returns Promise)\n if (this.redisClientType === REDIS_V4 || this.redisClientType === IOREDIS) {\n try {\n return this.redisClient.info(section)\n } catch (err) {\n throw new Error(`Failed to get Redis INFO: ${err.message}`)\n }\n }\n\n throw new Error('Unsupported Redis client type')\n }\n\n parseRedisConnections = clientsStr => {\n return clientsStr\n .split('\\n')\n .filter(line => line.trim() !== '')\n .map(line => {\n const parts = line.split(' ')\n const client = {}\n parts.forEach(p => {\n const eqIdx = p.indexOf('=')\n if (eqIdx === -1) return\n const k = p.slice(0, eqIdx)\n const v = p.slice(eqIdx + 1)\n if (redisConnectionFields.includes(k)) {\n client[k] = v\n }\n })\n return client\n })\n }\n\n /**\n * Collect basic Redis INFO metrics: clients, memory, stats\n * @returns {Promise<void>}\n */\n collectRedisMetrics = async () => {\n try {\n const [memoryInfoStr, statsInfoStr, connectionsInfoStr] =\n await Promise.all([\n this.getRedisInfo('memory'),\n this.getRedisInfo('stats'),\n this.getRedisConnections(),\n ])\n\n const labels = this.getDefaultLabels()\n\n const connections = this.parseRedisConnections(connectionsInfoStr)\n\n this.redisConnectionsGauge.reset()\n this.redisConnectionsMemoryGauge.reset()\n\n if (this.logValues) {\n // \"IN\" data: raw client list from Redis\n console.log('[queue-metrics] Redis CLIENT LIST (raw):\\n', connectionsInfoStr)\n console.log(\n '[queue-metrics] Redis CLIENT LIST (raw) length:',\n connectionsInfoStr.length\n )\n\n // \"OUT\" data: parsed fields used for metrics\n console.log(\n '[queue-metrics] Redis CLIENT LIST (parsed) count:',\n connections.length\n )\n console.log(\n '[queue-metrics] Redis CLIENT LIST (parsed sample, first 10):',\n JSON.stringify(connections.slice(0, 10), null, 2)\n )\n\n const missing = { name: 0, flags: 0, cmd: 0, 'tot-mem': 0 }\n connections.forEach(c => {\n if (!c.name) missing.name += 1\n if (!c.flags) missing.flags += 1\n if (!c.cmd) missing.cmd += 1\n if (!c['tot-mem']) missing['tot-mem'] += 1\n })\n console.log(\n '[queue-metrics] Redis CLIENT LIST (parsed) missing fields:',\n missing\n )\n }\n\n const grouped = {}\n connections.forEach(conn => {\n const { name, flags, 'tot-mem': totMem, cmd } = conn\n\n // build grouping key (includes default gauge labels later)\n const key = JSON.stringify({ name, flags, cmd })\n\n if (!grouped[key]) {\n grouped[key] = {\n labels: { name, flags, cmd },\n count: 0,\n memory: 0,\n }\n }\n\n grouped[key].count += 1\n grouped[key].memory += parseInt(totMem, 10)\n })\n\n if (this.logValues) {\n const groups = Object.values(grouped)\n const groupedTotal = groups.reduce((sum, g) => sum + (g.count || 0), 0)\n console.log('[queue-metrics] Redis connections grouped buckets:', groups.length)\n console.log('[queue-metrics] Redis connections grouped total:', groupedTotal)\n console.log(\n '[queue-metrics] Redis connections grouped sample (first 10):',\n JSON.stringify(groups.slice(0, 10), null, 2)\n )\n }\n\n Object.values(grouped).forEach(\n ({ labels: valueLabels, count, memory }) => {\n this.redisConnectionsGauge.set({ ...labels, ...valueLabels }, count)\n this.redisConnectionsMemoryGauge.set(\n { ...labels, ...valueLabels },\n memory\n )\n }\n )\n\n const parseRedisInfo = infoStr =>\n Object.fromEntries(\n infoStr\n .split('\\r\\n')\n .filter(line => line && !line.startsWith('#'))\n .map(line => line.split(':', 2))\n .filter(parts => parts.length === 2 && parts[0] && parts[1])\n )\n\n const memory = parseRedisInfo(memoryInfoStr)\n const stats = parseRedisInfo(statsInfoStr)\n\n if (memory.used_memory) {\n this.redisMemoryGauge.set(\n { ...labels, memory_type: 'used' },\n parseInt(memory.used_memory, 10) || 0\n )\n }\n if (memory.maxmemory) {\n this.redisMemoryGauge.set(\n { ...labels, memory_type: 'max' },\n parseInt(memory.maxmemory, 10) || 0\n )\n }\n\n if (stats.instantaneous_ops_per_sec) {\n this.redisStatsGauge.set(\n { ...labels, operation: 'ops_per_sec' },\n parseInt(stats.instantaneous_ops_per_sec, 10) || 0\n )\n }\n } catch (error) {\n console.warn(\n `[queue-metrics] Failed to collect Redis metrics:`,\n error.message\n )\n }\n }\n\n /**\n * Collect metrics for all Redis and push to Prometheus Pushgateway\n * @returns {Promise<void>}\n */\n pushRedisMetrics = async () => {\n try {\n await this.collectRedisMetrics()\n await this.gatewayPush()\n this.clearAllCounters()\n\n if (this.metricsLogValues) {\n const metricObjects = await this.registry.getMetricsAsJSON()\n console.info(\n `[queue-metrics] Collected metrics for Redis`,\n JSON.stringify(metricObjects, null, 2)\n )\n }\n } catch (error) {\n console.error(\n `[queue-metrics] Failed to collect Redis metrics: ${error.message}`\n )\n throw error\n }\n }\n\n /**\n * Start periodic collection.\n * @param {number} [intervalSec=this.intervalSec] - Interval in seconds\n */\n startPush = (intervalSec = this.intervalSec) => {\n this._startPush(intervalSec, () => {\n this.pushRedisMetrics().catch(err => {\n console.error(`[queue-metrics] Failed to push Redis metrics:`, err)\n })\n })\n }\n\n /**\n * Cleanup Redis client and exit process.\n * @returns {Promise<void>}\n */\n cleanup = async () => {\n try {\n if (!this.redisClient) return\n\n if (\n this.redisClientType === REDIS_V3 ||\n this.redisClientType === REDIS_V4\n ) {\n await this.redisClient.quit()\n } else if (this.redisClientType === IOREDIS) {\n await this.redisClient.disconnect()\n }\n } catch (err) {\n console.error('[queue-metrics] Error closing Redis client:', err)\n }\n process.exit(0)\n }\n\n _setCleanupHandlers = () => {\n process.on('SIGINT', this.cleanup)\n process.on('SIGTERM', this.cleanup)\n }\n}\n\nmodule.exports = { RedisMetricsClient }\n"],"mappings":";;AAAA,MAAM;EAAEA;AAAkB,CAAC,GAAGC,OAAO,CAAC,qBAAqB,CAAC;AAC5D,MAAM;EACJC,kBAAkB;EAClBC,QAAQ;EACRC,OAAO;EACPC;AACF,CAAC,GAAGJ,OAAO,CAAC,eAAe,CAAC;AAE5B,MAAMK,2BAA2B,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC;AAC5D,MAAMC,qBAAqB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC;;AAEjE;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,kBAAkB,SAASR,iBAAiB,CAAC;EACjD;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACES,WAAWA,CAAC;IAAEC,WAAW;IAAE,GAAGC;EAAc,CAAC,GAAG,CAAC,CAAC,EAAE;IAClD,MAAMC,WAAW,GACfD,aAAa,CAACC,WAAW,IACzBC,QAAQ,CAACC,OAAO,CAACC,GAAG,CAACC,0BAA0B,IAAI,EAAE,EAAE,EAAE,CAAC,IAC1D,CAAC;IAEH,KAAK,CAAC;MACJ,GAAGL,aAAa;MAChBM,WAAW,EAAEN,aAAa,CAACM,WAAW,IAAI,eAAe;MACzDL;IACF,CAAC,CAAC;;IAEF;IACA,IAAI,CAACF,WAAW,GAAGA,WAAW;IAC9B,IAAI,CAACQ,eAAe,GAAGhB,kBAAkB,CAACQ,WAAW,CAAC;;IAEtD;IACA,IAAI,CAACS,qBAAqB,GAAG,IAAI,CAACC,WAAW,CAAC;MAC5CC,IAAI,EAAE,6BAA6B;MACnCC,IAAI,EAAE,0BAA0B;MAChCC,UAAU,EAAE,IAAI,CAACC,iBAAiB,CAAClB,2BAA2B;IAChE,CAAC,CAAC;IAEF,IAAI,CAACmB,2BAA2B,GAAG,IAAI,CAACL,WAAW,CAAC;MAClDC,IAAI,EAAE,0CAA0C;MAChDC,IAAI,EAAE,0BAA0B;MAChCC,UAAU,EAAE,IAAI,CAACC,iBAAiB,CAAClB,2BAA2B;IAChE,CAAC,CAAC;;IAEF;IACA,IAAI,CAACoB,gBAAgB,GAAG,IAAI,CAACN,WAAW,CAAC;MACvCC,IAAI,EAAE,wBAAwB;MAC9BC,IAAI,EAAE,6BAA6B;MACnCC,UAAU,EAAE,IAAI,CAACC,iBAAiB,CAAC,CAAC,aAAa,CAAC;IACpD,CAAC,CAAC;;IAEF;IACA,IAAI,CAACG,eAAe,GAAG,IAAI,CAACP,WAAW,CAAC;MACtCC,IAAI,EAAE,uBAAuB;MAC7BC,IAAI,EAAE,4BAA4B;MAClCC,UAAU,EAAE,IAAI,CAACC,iBAAiB,CAAC,CAAC,WAAW,CAAC;IAClD,CAAC,CAAC;IAEF,IAAI,CAACI,mBAAmB,CAAC,CAAC;EAC5B;EAEAC,mBAAmB,GAAG,MAAAA,CAAA,KAAY;IAChC,IAAI,CAAC,IAAI,CAACnB,WAAW,EAAE,MAAM,IAAIoB,KAAK,CAAC,2BAA2B,CAAC;;IAEnE;IACA,IAAI,IAAI,CAACZ,eAAe,KAAKb,QAAQ,EAAE;MACrC,OAAO,IAAI0B,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;QACtC,IAAI,CAACvB,WAAW,CAACwB,YAAY,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,EAAE,CAACC,GAAG,EAAEC,MAAM,KAAK;UACjE,IAAID,GAAG,EAAE;YACPF,MAAM,CAAC,IAAIH,KAAK,CAAC,8BAA8BK,GAAG,CAACE,OAAO,EAAE,CAAC,CAAC;UAChE,CAAC,MAAML,OAAO,CAACI,MAAM,CAAC;QACxB,CAAC,CAAC;MACJ,CAAC,CAAC;IACJ;;IAEA;IACA,IAAI,IAAI,CAAClB,eAAe,KAAKf,QAAQ,EAAE;MACrC,IAAI;QACF,OAAO,IAAI,CAACO,WAAW,CAAC4B,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;MACzD,CAAC,CAAC,OAAOH,GAAG,EAAE;QACZ,MAAM,IAAIL,KAAK,CAAC,8BAA8BK,GAAG,CAACE,OAAO,EAAE,CAAC;MAC9D;IACF;IAEA,IAAI,IAAI,CAACnB,eAAe,KAAKd,OAAO,EAAE;MACpC,IAAI;QACF,OAAO,IAAI,CAACM,WAAW,CAAC6B,MAAM,CAAC,MAAM,CAAC;MACxC,CAAC,CAAC,OAAOJ,GAAG,EAAE;QACZ,MAAM,IAAIL,KAAK,CAAC,8BAA8BK,GAAG,CAACE,OAAO,EAAE,CAAC;MAC9D;IACF;IAEA,MAAM,IAAIP,KAAK,CAAC,+BAA+B,CAAC;EAClD,CAAC;EAEDU,YAAY,GAAG,MAAMC,OAAO,IAAI;IAC9B,IAAI,CAAC,IAAI,CAAC/B,WAAW,EAAE,MAAM,IAAIoB,KAAK,CAAC,2BAA2B,CAAC;;IAEnE;IACA,IAAI,IAAI,CAACZ,eAAe,KAAKb,QAAQ,EAAE;MACrC,OAAO,IAAI0B,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;QACtC,IAAI,CAACvB,WAAW,CAACgC,IAAI,CAACD,OAAO,EAAE,CAACN,GAAG,EAAEC,MAAM,KAAK;UAC9C,IAAID,GAAG,EAAEF,MAAM,CAACE,GAAG,CAAC,MACfH,OAAO,CAACI,MAAM,CAAC;QACtB,CAAC,CAAC;MACJ,CAAC,CAAC;IACJ;;IAEA;IACA,IAAI,IAAI,CAAClB,eAAe,KAAKf,QAAQ,IAAI,IAAI,CAACe,eAAe,KAAKd,OAAO,EAAE;MACzE,IAAI;QACF,OAAO,IAAI,CAACM,WAAW,CAACgC,IAAI,CAACD,OAAO,CAAC;MACvC,CAAC,CAAC,OAAON,GAAG,EAAE;QACZ,MAAM,IAAIL,KAAK,CAAC,6BAA6BK,GAAG,CAACE,OAAO,EAAE,CAAC;MAC7D;IACF;IAEA,MAAM,IAAIP,KAAK,CAAC,+BAA+B,CAAC;EAClD,CAAC;EAEDa,qBAAqB,GAAGC,UAAU,IAAI;IACpC,OAAOA,UAAU,CACdC,KAAK,CAAC,IAAI,CAAC,CACXC,MAAM,CAACC,IAAI,IAAIA,IAAI,CAACC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAClCC,GAAG,CAACF,IAAI,IAAI;MACX,MAAMG,KAAK,GAAGH,IAAI,CAACF,KAAK,CAAC,GAAG,CAAC;MAC7B,MAAMN,MAAM,GAAG,CAAC,CAAC;MACjBW,KAAK,CAACC,OAAO,CAACC,CAAC,IAAI;QACjB,MAAMC,KAAK,GAAGD,CAAC,CAACE,OAAO,CAAC,GAAG,CAAC;QAC5B,IAAID,KAAK,KAAK,CAAC,CAAC,EAAE;QAClB,MAAME,CAAC,GAAGH,CAAC,CAACI,KAAK,CAAC,CAAC,EAAEH,KAAK,CAAC;QAC3B,MAAMI,CAAC,GAAGL,CAAC,CAACI,KAAK,CAACH,KAAK,GAAG,CAAC,CAAC;QAC5B,IAAI9C,qBAAqB,CAACmD,QAAQ,CAACH,CAAC,CAAC,EAAE;UACrChB,MAAM,CAACgB,CAAC,CAAC,GAAGE,CAAC;QACf;MACF,CAAC,CAAC;MACF,OAAOlB,MAAM;IACf,CAAC,CAAC;EACN,CAAC;;EAED;AACF;AACA;AACA;EACEoB,mBAAmB,GAAG,MAAAA,CAAA,KAAY;IAChC,IAAI;MACF,MAAM,CAACC,aAAa,EAAEC,YAAY,EAAEC,kBAAkB,CAAC,GACrD,MAAM/B,OAAO,CAACgC,GAAG,CAAC,CAChB,IAAI,CAACvB,YAAY,CAAC,QAAQ,CAAC,EAC3B,IAAI,CAACA,YAAY,CAAC,OAAO,CAAC,EAC1B,IAAI,CAACX,mBAAmB,CAAC,CAAC,CAC3B,CAAC;MAEJ,MAAMmC,MAAM,GAAG,IAAI,CAACC,gBAAgB,CAAC,CAAC;MAEtC,MAAMC,WAAW,GAAG,IAAI,CAACvB,qBAAqB,CAACmB,kBAAkB,CAAC;MAElE,IAAI,CAAC3C,qBAAqB,CAACgD,KAAK,CAAC,CAAC;MAClC,IAAI,CAAC1C,2BAA2B,CAAC0C,KAAK,CAAC,CAAC;MAExC,IAAI,IAAI,CAACC,SAAS,EAAE;QAClB;QACAC,OAAO,CAACC,GAAG,CAAC,4CAA4C,EAAER,kBAAkB,CAAC;QAC7EO,OAAO,CAACC,GAAG,CACT,iDAAiD,EACjDR,kBAAkB,CAACS,MACrB,CAAC;;QAED;QACAF,OAAO,CAACC,GAAG,CACT,mDAAmD,EACnDJ,WAAW,CAACK,MACd,CAAC;QACDF,OAAO,CAACC,GAAG,CACT,8DAA8D,EAC9DE,IAAI,CAACC,SAAS,CAACP,WAAW,CAACV,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAClD,CAAC;QAED,MAAMkB,OAAO,GAAG;UAAErD,IAAI,EAAE,CAAC;UAAEsD,KAAK,EAAE,CAAC;UAAEC,GAAG,EAAE,CAAC;UAAE,SAAS,EAAE;QAAE,CAAC;QAC3DV,WAAW,CAACf,OAAO,CAAC0B,CAAC,IAAI;UACvB,IAAI,CAACA,CAAC,CAACxD,IAAI,EAAEqD,OAAO,CAACrD,IAAI,IAAI,CAAC;UAC9B,IAAI,CAACwD,CAAC,CAACF,KAAK,EAAED,OAAO,CAACC,KAAK,IAAI,CAAC;UAChC,IAAI,CAACE,CAAC,CAACD,GAAG,EAAEF,OAAO,CAACE,GAAG,IAAI,CAAC;UAC5B,IAAI,CAACC,CAAC,CAAC,SAAS,CAAC,EAAEH,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC;QAC5C,CAAC,CAAC;QACFL,OAAO,CAACC,GAAG,CACT,4DAA4D,EAC5DI,OACF,CAAC;MACH;MAEA,MAAMI,OAAO,GAAG,CAAC,CAAC;MAClBZ,WAAW,CAACf,OAAO,CAAC4B,IAAI,IAAI;QAC1B,MAAM;UAAE1D,IAAI;UAAEsD,KAAK;UAAE,SAAS,EAAEK,MAAM;UAAEJ;QAAI,CAAC,GAAGG,IAAI;;QAEpD;QACA,MAAME,GAAG,GAAGT,IAAI,CAACC,SAAS,CAAC;UAAEpD,IAAI;UAAEsD,KAAK;UAAEC;QAAI,CAAC,CAAC;QAEhD,IAAI,CAACE,OAAO,CAACG,GAAG,CAAC,EAAE;UACjBH,OAAO,CAACG,GAAG,CAAC,GAAG;YACbjB,MAAM,EAAE;cAAE3C,IAAI;cAAEsD,KAAK;cAAEC;YAAI,CAAC;YAC5BM,KAAK,EAAE,CAAC;YACRC,MAAM,EAAE;UACV,CAAC;QACH;QAEAL,OAAO,CAACG,GAAG,CAAC,CAACC,KAAK,IAAI,CAAC;QACvBJ,OAAO,CAACG,GAAG,CAAC,CAACE,MAAM,IAAItE,QAAQ,CAACmE,MAAM,EAAE,EAAE,CAAC;MAC7C,CAAC,CAAC;MAEF,IAAI,IAAI,CAACZ,SAAS,EAAE;QAClB,MAAMgB,MAAM,GAAGC,MAAM,CAACC,MAAM,CAACR,OAAO,CAAC;QACrC,MAAMS,YAAY,GAAGH,MAAM,CAACI,MAAM,CAAC,CAACC,GAAG,EAAEC,CAAC,KAAKD,GAAG,IAAIC,CAAC,CAACR,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;QACvEb,OAAO,CAACC,GAAG,CAAC,oDAAoD,EAAEc,MAAM,CAACb,MAAM,CAAC;QAChFF,OAAO,CAACC,GAAG,CAAC,kDAAkD,EAAEiB,YAAY,CAAC;QAC7ElB,OAAO,CAACC,GAAG,CACT,8DAA8D,EAC9DE,IAAI,CAACC,SAAS,CAACW,MAAM,CAAC5B,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAC7C,CAAC;MACH;MAEA6B,MAAM,CAACC,MAAM,CAACR,OAAO,CAAC,CAAC3B,OAAO,CAC5B,CAAC;QAAEa,MAAM,EAAE2B,WAAW;QAAET,KAAK;QAAEC;MAAO,CAAC,KAAK;QAC1C,IAAI,CAAChE,qBAAqB,CAACyE,GAAG,CAAC;UAAE,GAAG5B,MAAM;UAAE,GAAG2B;QAAY,CAAC,EAAET,KAAK,CAAC;QACpE,IAAI,CAACzD,2BAA2B,CAACmE,GAAG,CAClC;UAAE,GAAG5B,MAAM;UAAE,GAAG2B;QAAY,CAAC,EAC7BR,MACF,CAAC;MACH,CACF,CAAC;MAED,MAAMU,cAAc,GAAGC,OAAO,IAC5BT,MAAM,CAACU,WAAW,CAChBD,OAAO,CACJjD,KAAK,CAAC,MAAM,CAAC,CACbC,MAAM,CAACC,IAAI,IAAIA,IAAI,IAAI,CAACA,IAAI,CAACiD,UAAU,CAAC,GAAG,CAAC,CAAC,CAC7C/C,GAAG,CAACF,IAAI,IAAIA,IAAI,CAACF,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAC/BC,MAAM,CAACI,KAAK,IAAIA,KAAK,CAACqB,MAAM,KAAK,CAAC,IAAIrB,KAAK,CAAC,CAAC,CAAC,IAAIA,KAAK,CAAC,CAAC,CAAC,CAC/D,CAAC;MAEH,MAAMiC,MAAM,GAAGU,cAAc,CAACjC,aAAa,CAAC;MAC5C,MAAMqC,KAAK,GAAGJ,cAAc,CAAChC,YAAY,CAAC;MAE1C,IAAIsB,MAAM,CAACe,WAAW,EAAE;QACtB,IAAI,CAACxE,gBAAgB,CAACkE,GAAG,CACvB;UAAE,GAAG5B,MAAM;UAAEmC,WAAW,EAAE;QAAO,CAAC,EAClCtF,QAAQ,CAACsE,MAAM,CAACe,WAAW,EAAE,EAAE,CAAC,IAAI,CACtC,CAAC;MACH;MACA,IAAIf,MAAM,CAACiB,SAAS,EAAE;QACpB,IAAI,CAAC1E,gBAAgB,CAACkE,GAAG,CACvB;UAAE,GAAG5B,MAAM;UAAEmC,WAAW,EAAE;QAAM,CAAC,EACjCtF,QAAQ,CAACsE,MAAM,CAACiB,SAAS,EAAE,EAAE,CAAC,IAAI,CACpC,CAAC;MACH;MAEA,IAAIH,KAAK,CAACI,yBAAyB,EAAE;QACnC,IAAI,CAAC1E,eAAe,CAACiE,GAAG,CACtB;UAAE,GAAG5B,MAAM;UAAEsC,SAAS,EAAE;QAAc,CAAC,EACvCzF,QAAQ,CAACoF,KAAK,CAACI,yBAAyB,EAAE,EAAE,CAAC,IAAI,CACnD,CAAC;MACH;IACF,CAAC,CAAC,OAAOE,KAAK,EAAE;MACdlC,OAAO,CAACmC,IAAI,CACV,kDAAkD,EAClDD,KAAK,CAAClE,OACR,CAAC;IACH;EACF,CAAC;;EAED;AACF;AACA;AACA;EACEoE,gBAAgB,GAAG,MAAAA,CAAA,KAAY;IAC7B,IAAI;MACF,MAAM,IAAI,CAAC9C,mBAAmB,CAAC,CAAC;MAChC,MAAM,IAAI,CAAC+C,WAAW,CAAC,CAAC;MACxB,IAAI,CAACC,gBAAgB,CAAC,CAAC;MAEvB,IAAI,IAAI,CAACC,gBAAgB,EAAE;QACzB,MAAMC,aAAa,GAAG,MAAM,IAAI,CAACC,QAAQ,CAACC,gBAAgB,CAAC,CAAC;QAC5D1C,OAAO,CAAC3B,IAAI,CACV,6CAA6C,EAC7C8B,IAAI,CAACC,SAAS,CAACoC,aAAa,EAAE,IAAI,EAAE,CAAC,CACvC,CAAC;MACH;IACF,CAAC,CAAC,OAAON,KAAK,EAAE;MACdlC,OAAO,CAACkC,KAAK,CACX,oDAAoDA,KAAK,CAAClE,OAAO,EACnE,CAAC;MACD,MAAMkE,KAAK;IACb;EACF,CAAC;;EAED;AACF;AACA;AACA;EACES,SAAS,GAAGA,CAACpG,WAAW,GAAG,IAAI,CAACA,WAAW,KAAK;IAC9C,IAAI,CAACqG,UAAU,CAACrG,WAAW,EAAE,MAAM;MACjC,IAAI,CAAC6F,gBAAgB,CAAC,CAAC,CAACS,KAAK,CAAC/E,GAAG,IAAI;QACnCkC,OAAO,CAACkC,KAAK,CAAC,+CAA+C,EAAEpE,GAAG,CAAC;MACrE,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ,CAAC;;EAED;AACF;AACA;AACA;EACEgF,OAAO,GAAG,MAAAA,CAAA,KAAY;IACpB,IAAI;MACF,IAAI,CAAC,IAAI,CAACzG,WAAW,EAAE;MAEvB,IACE,IAAI,CAACQ,eAAe,KAAKb,QAAQ,IACjC,IAAI,CAACa,eAAe,KAAKf,QAAQ,EACjC;QACA,MAAM,IAAI,CAACO,WAAW,CAAC0G,IAAI,CAAC,CAAC;MAC/B,CAAC,MAAM,IAAI,IAAI,CAAClG,eAAe,KAAKd,OAAO,EAAE;QAC3C,MAAM,IAAI,CAACM,WAAW,CAAC2G,UAAU,CAAC,CAAC;MACrC;IACF,CAAC,CAAC,OAAOlF,GAAG,EAAE;MACZkC,OAAO,CAACkC,KAAK,CAAC,6CAA6C,EAAEpE,GAAG,CAAC;IACnE;IACArB,OAAO,CAACwG,IAAI,CAAC,CAAC,CAAC;EACjB,CAAC;EAED1F,mBAAmB,GAAGA,CAAA,KAAM;IAC1Bd,OAAO,CAACyG,EAAE,CAAC,QAAQ,EAAE,IAAI,CAACJ,OAAO,CAAC;IAClCrG,OAAO,CAACyG,EAAE,CAAC,SAAS,EAAE,IAAI,CAACJ,OAAO,CAAC;EACrC,CAAC;AACH;AAEAK,MAAM,CAACC,OAAO,GAAG;EAAEjH;AAAmB,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"metricsRedisClient.js","names":["BaseMetricsClient","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","redisConnectionStableFields","redisConnectionFields","RedisMetricsClient","constructor","redisClient","metricsConfig","intervalSec","parseInt","process","env","METRICS_QUEUE_INTERVAL_SEC","processType","redisClientType","redisConnectionsGauge","createGauge","name","help","labelNames","withDefaultLabels","redisConnectionsMemoryGauge","redisMemoryGauge","redisStatsGauge","_redisConnSeenKeys","Set","_redisConnZeroedKeys","_setCleanupHandlers","getRedisConnections","Error","Promise","resolve","reject","send_command","err","result","message","sendCommand","client","getRedisInfo","section","info","parseRedisConnections","clientsStr","split","filter","line","trim","map","parts","forEach","p","eqIdx","indexOf","k","slice","v","includes","collectRedisMetrics","memoryInfoStr","statsInfoStr","connectionsInfoStr","all","labels","getDefaultLabels","connections","logValues","console","log","length","JSON","stringify","missing","flags","cmd","c","grouped","conn","totMem","key","count","memory","mem","Number","isFinite","currentKeys","Object","keys","add","has","delete","reset","valueLabels","parse","set","groups","values","groupedTotal","reduce","sum","g","size","parseRedisInfo","infoStr","fromEntries","startsWith","stats","used_memory","memory_type","maxmemory","instantaneous_ops_per_sec","operation","error","warn","pushRedisMetrics","gatewayPush","clearAllCounters","metricsLogValues","metricObjects","registry","getMetricsAsJSON","startPush","_startPush","catch","cleanup","quit","disconnect","exit","on","module","exports"],"sources":["../../src/metrics/metricsRedisClient.js"],"sourcesContent":["const { BaseMetricsClient } = require('./baseMetricsClient')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('../redisUtils')\n\nconst redisConnectionStableFields = ['name', 'flags', 'cmd']\nconst redisConnectionFields = ['name', 'flags', 'tot-mem', 'cmd']\n\n/**\n * RedisMetricsClient extends BaseMetricsClient to collect\n * Redis metrics periodically and push them to Prometheus Pushgateway.\n *\n * @extends BaseMetricsClient\n */\nclass RedisMetricsClient extends BaseMetricsClient {\n /**\n * @param {Object} options\n * @param {any} options.redisClient - Redis client instance (required)\n * @param {string} [options.appName] - Application name (from BaseMetricsClient)\n * @param {string} [options.dynoId] - Dyno/instance ID (from BaseMetricsClient)\n * @param {string} [options.processType] - Process type (from BaseMetricsClient)\n * @param {boolean} [options.enabled] - Enable metrics collection (from BaseMetricsClient)\n * @param {boolean} [options.logValues] - Log metrics values (from BaseMetricsClient)\n * @param {string} [options.pushgatewayUrl] - PushGateway URL (from BaseMetricsClient)\n * @param {string} [options.pushgatewaySecret] - PushGateway secret token (from BaseMetricsClient)\n * @param {number} [options.intervalSec] - Interval in seconds for pushing metrics (from BaseMetricsClient)\n * @param {boolean} [options.removeOldMetrics] - Remove old metrics by service (from BaseMetricsClient)\n * @param {function} [options.startupValidation] - Function to validate startup (from BaseMetricsClient)\n * @param {boolean} [options.disablePushgateway] - Disable pushing to Pushgateway (use HTTP scraping instead)\n */\n constructor({ redisClient, ...metricsConfig } = {}) {\n const intervalSec =\n metricsConfig.intervalSec ||\n parseInt(process.env.METRICS_QUEUE_INTERVAL_SEC || '', 10) ||\n 5\n\n super({\n ...metricsConfig,\n processType: metricsConfig.processType || 'queue-metrics',\n intervalSec,\n })\n\n /** Redis client used for metrics */\n this.redisClient = redisClient\n this.redisClientType = getRedisClientType(redisClient)\n\n /** Counter for Redis client connections */\n this.redisConnectionsGauge = this.createGauge({\n name: 'app_redis_connections_count',\n help: 'Redis client connections',\n labelNames: this.withDefaultLabels(redisConnectionStableFields),\n })\n\n this.redisConnectionsMemoryGauge = this.createGauge({\n name: 'app_redis_connections_memory_usage_count',\n help: 'Redis client connections',\n labelNames: this.withDefaultLabels(redisConnectionStableFields),\n })\n\n /** Gauge for Redis memory usage */\n this.redisMemoryGauge = this.createGauge({\n name: 'app_redis_memory_bytes',\n help: 'Redis memory usage in bytes',\n labelNames: this.withDefaultLabels(['memory_type']),\n })\n\n /** Gauge for Redis operation stats */\n this.redisStatsGauge = this.createGauge({\n name: 'app_redis_stats_total',\n help: 'Redis operation statistics',\n labelNames: this.withDefaultLabels(['operation']),\n })\n\n // Track emitted connection label combinations so we can:\n // - emit a one-shot 0 for series that disappear (overwrite last non-zero sample)\n // - then stop emitting them on subsequent scrapes\n //\n // This is mainly needed because we label connections by `cmd`,\n // and Redis `CLIENT LIST` `cmd` is volatile.\n this._redisConnSeenKeys = new Set()\n this._redisConnZeroedKeys = new Set()\n\n this._setCleanupHandlers()\n }\n\n getRedisConnections = async () => {\n if (!this.redisClient) throw new Error('Redis client not provided')\n\n // node-redis v3 (uses callback)\n if (this.redisClientType === REDIS_V3) {\n return new Promise((resolve, reject) => {\n this.redisClient.send_command('CLIENT', ['LIST'], (err, result) => {\n if (err) {\n reject(new Error(`Failed to get CLIENT LIST: ${err.message}`))\n } else resolve(result)\n })\n })\n }\n\n // node-redis v4\n if (this.redisClientType === REDIS_V4) {\n try {\n return this.redisClient.sendCommand(['CLIENT', 'LIST'])\n } catch (err) {\n throw new Error(`Failed to get CLIENT LIST: ${err.message}`)\n }\n }\n\n if (this.redisClientType === IOREDIS) {\n try {\n return this.redisClient.client('LIST')\n } catch (err) {\n throw new Error(`Failed to get CLIENT LIST: ${err.message}`)\n }\n }\n\n throw new Error('Unsupported Redis client type')\n }\n\n getRedisInfo = async section => {\n if (!this.redisClient) throw new Error('Redis client not provided')\n\n // node-redis v3 (uses callback)\n if (this.redisClientType === REDIS_V3) {\n return new Promise((resolve, reject) => {\n this.redisClient.info(section, (err, result) => {\n if (err) reject(err)\n else resolve(result)\n })\n })\n }\n\n // node-redis v4 or ioredis (info returns Promise)\n if (this.redisClientType === REDIS_V4 || this.redisClientType === IOREDIS) {\n try {\n return this.redisClient.info(section)\n } catch (err) {\n throw new Error(`Failed to get Redis INFO: ${err.message}`)\n }\n }\n\n throw new Error('Unsupported Redis client type')\n }\n\n parseRedisConnections = clientsStr => {\n return clientsStr\n .split('\\n')\n .filter(line => line.trim() !== '')\n .map(line => {\n const parts = line.split(' ')\n const client = {}\n parts.forEach(p => {\n const eqIdx = p.indexOf('=')\n if (eqIdx === -1) return\n const k = p.slice(0, eqIdx)\n const v = p.slice(eqIdx + 1)\n if (redisConnectionFields.includes(k)) {\n client[k] = v\n }\n })\n return client\n })\n }\n\n /**\n * Collect basic Redis INFO metrics: clients, memory, stats\n * @returns {Promise<void>}\n */\n collectRedisMetrics = async () => {\n try {\n const [memoryInfoStr, statsInfoStr, connectionsInfoStr] =\n await Promise.all([\n this.getRedisInfo('memory'),\n this.getRedisInfo('stats'),\n this.getRedisConnections(),\n ])\n\n const labels = this.getDefaultLabels()\n\n const connections = this.parseRedisConnections(connectionsInfoStr)\n\n if (this.logValues) {\n // \"IN\" data: raw client list from Redis\n console.log('[queue-metrics] Redis CLIENT LIST (raw):\\n', connectionsInfoStr)\n console.log(\n '[queue-metrics] Redis CLIENT LIST (raw) length:',\n connectionsInfoStr.length\n )\n\n // \"OUT\" data: parsed fields used for metrics\n console.log(\n '[queue-metrics] Redis CLIENT LIST (parsed) count:',\n connections.length\n )\n console.log(\n '[queue-metrics] Redis CLIENT LIST (parsed sample, first 10):',\n JSON.stringify(connections.slice(0, 10), null, 2)\n )\n\n const missing = { name: 0, flags: 0, cmd: 0, 'tot-mem': 0 }\n connections.forEach(c => {\n if (!c.name) missing.name += 1\n if (!c.flags) missing.flags += 1\n if (!c.cmd) missing.cmd += 1\n if (!c['tot-mem']) missing['tot-mem'] += 1\n })\n console.log(\n '[queue-metrics] Redis CLIENT LIST (parsed) missing fields:',\n missing\n )\n }\n\n const grouped = {}\n connections.forEach(conn => {\n const { name, flags, 'tot-mem': totMem, cmd } = conn\n\n // build grouping key (includes default gauge labels later)\n const key = JSON.stringify({ name, flags, cmd })\n\n if (!grouped[key]) {\n grouped[key] = {\n labels: { name, flags, cmd },\n count: 0,\n memory: 0,\n }\n }\n\n grouped[key].count += 1\n const mem = parseInt(totMem, 10)\n grouped[key].memory += Number.isFinite(mem) ? mem : 0\n })\n\n // Ensure sets exist even if older instance is hot-reloaded.\n if (!this._redisConnSeenKeys) this._redisConnSeenKeys = new Set()\n if (!this._redisConnZeroedKeys) this._redisConnZeroedKeys = new Set()\n\n const currentKeys = new Set(Object.keys(grouped))\n for (const k of currentKeys) {\n this._redisConnSeenKeys.add(k)\n if (this._redisConnZeroedKeys.has(k)) this._redisConnZeroedKeys.delete(k)\n }\n\n // Reset gauges each scrape so we only export:\n // - current snapshot series\n // - one-shot zeros for recently disappeared series\n this.redisConnectionsGauge.reset()\n this.redisConnectionsMemoryGauge.reset()\n\n // Emit one-shot zeros for series that disappeared since last seen.\n for (const k of this._redisConnSeenKeys) {\n if (currentKeys.has(k)) continue\n if (this._redisConnZeroedKeys.has(k)) continue\n try {\n const valueLabels = JSON.parse(k)\n this.redisConnectionsGauge.set({ ...labels, ...valueLabels }, 0)\n this.redisConnectionsMemoryGauge.set({ ...labels, ...valueLabels }, 0)\n this._redisConnZeroedKeys.add(k)\n } catch {\n // ignore malformed key\n }\n }\n\n if (this.logValues) {\n const groups = Object.values(grouped)\n const groupedTotal = groups.reduce((sum, g) => sum + (g.count || 0), 0)\n console.log('[queue-metrics] Redis connections grouped buckets:', groups.length)\n console.log('[queue-metrics] Redis connections grouped total:', groupedTotal)\n console.log(\n '[queue-metrics] Redis connections seen keys:',\n this._redisConnSeenKeys.size\n )\n console.log(\n '[queue-metrics] Redis connections zeroed keys:',\n this._redisConnZeroedKeys.size\n )\n console.log(\n '[queue-metrics] Redis connections grouped sample (first 10):',\n JSON.stringify(groups.slice(0, 10), null, 2)\n )\n }\n\n Object.values(grouped).forEach(\n ({ labels: valueLabels, count, memory }) => {\n this.redisConnectionsGauge.set({ ...labels, ...valueLabels }, count)\n this.redisConnectionsMemoryGauge.set(\n { ...labels, ...valueLabels },\n memory\n )\n }\n )\n\n const parseRedisInfo = infoStr =>\n Object.fromEntries(\n infoStr\n .split('\\r\\n')\n .filter(line => line && !line.startsWith('#'))\n .map(line => line.split(':', 2))\n .filter(parts => parts.length === 2 && parts[0] && parts[1])\n )\n\n const memory = parseRedisInfo(memoryInfoStr)\n const stats = parseRedisInfo(statsInfoStr)\n\n if (memory.used_memory) {\n this.redisMemoryGauge.set(\n { ...labels, memory_type: 'used' },\n parseInt(memory.used_memory, 10) || 0\n )\n }\n if (memory.maxmemory) {\n this.redisMemoryGauge.set(\n { ...labels, memory_type: 'max' },\n parseInt(memory.maxmemory, 10) || 0\n )\n }\n\n if (stats.instantaneous_ops_per_sec) {\n this.redisStatsGauge.set(\n { ...labels, operation: 'ops_per_sec' },\n parseInt(stats.instantaneous_ops_per_sec, 10) || 0\n )\n }\n } catch (error) {\n console.warn(\n `[queue-metrics] Failed to collect Redis metrics:`,\n error.message\n )\n }\n }\n\n /**\n * Collect metrics for all Redis and push to Prometheus Pushgateway\n * @returns {Promise<void>}\n */\n pushRedisMetrics = async () => {\n try {\n await this.collectRedisMetrics()\n await this.gatewayPush()\n this.clearAllCounters()\n\n if (this.metricsLogValues) {\n const metricObjects = await this.registry.getMetricsAsJSON()\n console.info(\n `[queue-metrics] Collected metrics for Redis`,\n JSON.stringify(metricObjects, null, 2)\n )\n }\n } catch (error) {\n console.error(\n `[queue-metrics] Failed to collect Redis metrics: ${error.message}`\n )\n throw error\n }\n }\n\n /**\n * Start periodic collection.\n * @param {number} [intervalSec=this.intervalSec] - Interval in seconds\n */\n startPush = (intervalSec = this.intervalSec) => {\n this._startPush(intervalSec, () => {\n this.pushRedisMetrics().catch(err => {\n console.error(`[queue-metrics] Failed to push Redis metrics:`, err)\n })\n })\n }\n\n /**\n * Cleanup Redis client and exit process.\n * @returns {Promise<void>}\n */\n cleanup = async () => {\n try {\n if (!this.redisClient) return\n\n if (\n this.redisClientType === REDIS_V3 ||\n this.redisClientType === REDIS_V4\n ) {\n await this.redisClient.quit()\n } else if (this.redisClientType === IOREDIS) {\n await this.redisClient.disconnect()\n }\n } catch (err) {\n console.error('[queue-metrics] Error closing Redis client:', err)\n }\n process.exit(0)\n }\n\n _setCleanupHandlers = () => {\n process.on('SIGINT', this.cleanup)\n process.on('SIGTERM', this.cleanup)\n }\n}\n\nmodule.exports = { RedisMetricsClient }\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.148",
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
  }
@@ -74,6 +74,15 @@ class RedisMetricsClient extends BaseMetricsClient {
74
74
  labelNames: this.withDefaultLabels(['operation']),
75
75
  })
76
76
 
77
+ // Track emitted connection label combinations so we can:
78
+ // - emit a one-shot 0 for series that disappear (overwrite last non-zero sample)
79
+ // - then stop emitting them on subsequent scrapes
80
+ //
81
+ // This is mainly needed because we label connections by `cmd`,
82
+ // and Redis `CLIENT LIST` `cmd` is volatile.
83
+ this._redisConnSeenKeys = new Set()
84
+ this._redisConnZeroedKeys = new Set()
85
+
77
86
  this._setCleanupHandlers()
78
87
  }
79
88
 
@@ -173,9 +182,6 @@ class RedisMetricsClient extends BaseMetricsClient {
173
182
 
174
183
  const connections = this.parseRedisConnections(connectionsInfoStr)
175
184
 
176
- this.redisConnectionsGauge.reset()
177
- this.redisConnectionsMemoryGauge.reset()
178
-
179
185
  if (this.logValues) {
180
186
  // "IN" data: raw client list from Redis
181
187
  console.log('[queue-metrics] Redis CLIENT LIST (raw):\n', connectionsInfoStr)
@@ -223,14 +229,53 @@ class RedisMetricsClient extends BaseMetricsClient {
223
229
  }
224
230
 
225
231
  grouped[key].count += 1
226
- grouped[key].memory += parseInt(totMem, 10)
232
+ const mem = parseInt(totMem, 10)
233
+ grouped[key].memory += Number.isFinite(mem) ? mem : 0
227
234
  })
228
235
 
236
+ // Ensure sets exist even if older instance is hot-reloaded.
237
+ if (!this._redisConnSeenKeys) this._redisConnSeenKeys = new Set()
238
+ if (!this._redisConnZeroedKeys) this._redisConnZeroedKeys = new Set()
239
+
240
+ const currentKeys = new Set(Object.keys(grouped))
241
+ for (const k of currentKeys) {
242
+ this._redisConnSeenKeys.add(k)
243
+ if (this._redisConnZeroedKeys.has(k)) this._redisConnZeroedKeys.delete(k)
244
+ }
245
+
246
+ // Reset gauges each scrape so we only export:
247
+ // - current snapshot series
248
+ // - one-shot zeros for recently disappeared series
249
+ this.redisConnectionsGauge.reset()
250
+ this.redisConnectionsMemoryGauge.reset()
251
+
252
+ // Emit one-shot zeros for series that disappeared since last seen.
253
+ for (const k of this._redisConnSeenKeys) {
254
+ if (currentKeys.has(k)) continue
255
+ if (this._redisConnZeroedKeys.has(k)) continue
256
+ try {
257
+ const valueLabels = JSON.parse(k)
258
+ this.redisConnectionsGauge.set({ ...labels, ...valueLabels }, 0)
259
+ this.redisConnectionsMemoryGauge.set({ ...labels, ...valueLabels }, 0)
260
+ this._redisConnZeroedKeys.add(k)
261
+ } catch {
262
+ // ignore malformed key
263
+ }
264
+ }
265
+
229
266
  if (this.logValues) {
230
267
  const groups = Object.values(grouped)
231
268
  const groupedTotal = groups.reduce((sum, g) => sum + (g.count || 0), 0)
232
269
  console.log('[queue-metrics] Redis connections grouped buckets:', groups.length)
233
270
  console.log('[queue-metrics] Redis connections grouped total:', groupedTotal)
271
+ console.log(
272
+ '[queue-metrics] Redis connections seen keys:',
273
+ this._redisConnSeenKeys.size
274
+ )
275
+ console.log(
276
+ '[queue-metrics] Redis connections zeroed keys:',
277
+ this._redisConnZeroedKeys.size
278
+ )
234
279
  console.log(
235
280
  '[queue-metrics] Redis connections grouped sample (first 10):',
236
281
  JSON.stringify(groups.slice(0, 10), null, 2)