@adalo/metrics 0.1.119 → 0.1.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -1,8 +1,11 @@
1
- BUILD_APP_NAME=
2
- HOSTNAME=
3
- BUILD_DYNO_PROCESS_TYPE=
4
- METRICS_ENABLED=
5
- METRICS_LOG_VALUES=
6
- METRICS_PUSHGATEWAY_URL=
7
- METRICS_PUSHGATEWAY_SECRET=
8
- METRICS_INTERVAL_SEC=
1
+ BUILD_APP_NAME="staging"
2
+ HOSTNAME="staging-queue-metricus"
3
+ BUILD_DYNO_PROCESS_TYPE="web"
4
+ METRICS_ENABLED=true
5
+ METRICS_LOG_VALUES=true
6
+ METRICS_PUSHGATEWAY_URL=https://pushgateway.infradalogs.adalo.com
7
+ METRICS_PUSHGATEWAY_SECRET="METRICS_PUSHGATEWAY_SECRET"
8
+ METRICS_INTERVAL_SEC=60
9
+
10
+
11
+
package/README.md CHANGED
@@ -48,6 +48,60 @@ metricsClient.gatewayPush({ groupings: { process_type: 'web' } })
48
48
  metricsClient.startPush(15) // interval in seconds
49
49
  ```
50
50
 
51
+ ## HealthCheckClient
52
+
53
+ Provides a health check endpoint for external monitoring services like BetterStack.
54
+ Validates database and Redis connections with rate limiting to prevent excessive load.
55
+
56
+ ```typescript
57
+ import { HealthCheckClient } from '@adalo/metrics'
58
+ import Redis from 'ioredis'
59
+
60
+ const redisClient = new Redis()
61
+ const healthCheck = new HealthCheckClient({
62
+ databaseUrl: process.env.DATABASE_URL,
63
+ databaseName: 'main',
64
+ additionalDatabaseUrls: {
65
+ cluster_1: process.env.CLUSTER_1_URL,
66
+ cluster_2: process.env.CLUSTER_2_URL,
67
+ },
68
+ redisClient,
69
+ cacheTtlMs: 60000, // Cache results for 60 seconds (default)
70
+ })
71
+
72
+ // Register on Express app
73
+ healthCheck.registerHealthEndpoint(app, '/betterstack-health')
74
+ ```
75
+
76
+ Response format (BetterStack compatible):
77
+ ```json
78
+ {
79
+ "status": "healthy",
80
+ "timestamp": "2026-01-20T12:00:00.000Z",
81
+ "cached": false,
82
+ "components": {
83
+ "database": {
84
+ "status": "healthy",
85
+ "clusters": {
86
+ "main": { "status": "healthy", "latencyMs": 5 },
87
+ "cluster_1": { "status": "healthy", "latencyMs": 8 },
88
+ "cluster_2": { "status": "healthy", "latencyMs": 6 }
89
+ }
90
+ },
91
+ "redis": { "status": "healthy", "latencyMs": 2 }
92
+ }
93
+ }
94
+ ```
95
+
96
+ Status codes:
97
+ - `200` - healthy or degraded
98
+ - `503` - unhealthy (at least one component failed)
99
+
100
+ Environment variables:
101
+ | Variable | Description | Default |
102
+ |----------|-------------|---------|
103
+ | `DATABASE_URL` | Main PostgreSQL connection URL | - |
104
+
51
105
  ## Tips
52
106
  secret env was created as
53
107
  ```js
@@ -0,0 +1,212 @@
1
+ export type HealthStatus = 'healthy' | 'unhealthy' | 'degraded';
2
+ export type ComponentHealth = {
3
+ /**
4
+ * - Component health status
5
+ */
6
+ status: HealthStatus;
7
+ /**
8
+ * - Optional status message
9
+ */
10
+ message?: string | undefined;
11
+ /**
12
+ * - Connection latency in milliseconds
13
+ */
14
+ latencyMs?: number | undefined;
15
+ };
16
+ export type DatabaseClusterHealth = {
17
+ /**
18
+ * - Overall databases status
19
+ */
20
+ status: HealthStatus;
21
+ /**
22
+ * - Individual cluster health
23
+ */
24
+ clusters: {
25
+ [x: string]: ComponentHealth;
26
+ };
27
+ };
28
+ export type HealthCheckResult = {
29
+ /**
30
+ * - Overall health status
31
+ */
32
+ status: HealthStatus;
33
+ /**
34
+ * - ISO timestamp of the check
35
+ */
36
+ timestamp: string;
37
+ /**
38
+ * - Whether this result is from cache
39
+ */
40
+ cached: boolean;
41
+ /**
42
+ * - Individual component health
43
+ */
44
+ components: {
45
+ [x: string]: ComponentHealth | DatabaseClusterHealth;
46
+ };
47
+ };
48
+ export type CachedHealthResult = {
49
+ /**
50
+ * - The cached health check result
51
+ */
52
+ result: HealthCheckResult;
53
+ /**
54
+ * - Unix timestamp when cached
55
+ */
56
+ timestamp: number;
57
+ };
58
+ export type DatabaseConfig = {
59
+ /**
60
+ * - Database/cluster name
61
+ */
62
+ name: string;
63
+ /**
64
+ * - Connection URL
65
+ */
66
+ url: string;
67
+ };
68
+ /**
69
+ * @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus
70
+ */
71
+ /**
72
+ * @typedef {Object} ComponentHealth
73
+ * @property {HealthStatus} status - Component health status
74
+ * @property {string} [message] - Optional status message
75
+ * @property {number} [latencyMs] - Connection latency in milliseconds
76
+ */
77
+ /**
78
+ * @typedef {Object} DatabaseClusterHealth
79
+ * @property {HealthStatus} status - Overall databases status
80
+ * @property {Object<string, ComponentHealth>} clusters - Individual cluster health
81
+ */
82
+ /**
83
+ * @typedef {Object} HealthCheckResult
84
+ * @property {HealthStatus} status - Overall health status
85
+ * @property {string} timestamp - ISO timestamp of the check
86
+ * @property {boolean} cached - Whether this result is from cache
87
+ * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health
88
+ */
89
+ /**
90
+ * @typedef {Object} CachedHealthResult
91
+ * @property {HealthCheckResult} result - The cached health check result
92
+ * @property {number} timestamp - Unix timestamp when cached
93
+ */
94
+ /**
95
+ * @typedef {Object} DatabaseConfig
96
+ * @property {string} name - Database/cluster name
97
+ * @property {string} url - Connection URL
98
+ */
99
+ /**
100
+ * HealthCheckClient provides a health check middleware for external monitoring services
101
+ * like BetterStack. It validates database and Redis connections with rate limiting
102
+ * to prevent excessive load on backend services.
103
+ *
104
+ * Features:
105
+ * - Multi-cluster DB validation (PostgreSQL)
106
+ * - Redis connection validation (supports ioredis, node-redis v3/v4)
107
+ * - Result caching (default: 60 seconds) to prevent overloading services
108
+ * - Express middleware support
109
+ * - BetterStack-compatible JSON response format
110
+ */
111
+ export class HealthCheckClient {
112
+ /**
113
+ * @param {Object} options
114
+ * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL
115
+ * @param {string} [options.databaseName='main'] - Name for the main database
116
+ * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)
117
+ * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)
118
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)
119
+ * @param {string} [options.appName] - Application name for logging
120
+ */
121
+ constructor(options?: {
122
+ databaseUrl?: string | undefined;
123
+ databaseName?: string | undefined;
124
+ additionalDatabaseUrls?: {
125
+ [x: string]: string;
126
+ } | undefined;
127
+ redisClient?: any;
128
+ cacheTtlMs?: number | undefined;
129
+ appName?: string | undefined;
130
+ });
131
+ redisClient: any;
132
+ cacheTtlMs: number;
133
+ appName: string;
134
+ prefixLogs: string;
135
+ /** @type {CachedHealthResult | null} */
136
+ _cachedResult: CachedHealthResult | null;
137
+ /** @type {Map<string, Pool>} */
138
+ _databasePools: Map<string, Pool>;
139
+ /** @type {DatabaseConfig[]} */
140
+ _databaseConfigs: DatabaseConfig[];
141
+ _redisClientType: string | undefined;
142
+ /**
143
+ * Initialize database configurations from options.
144
+ * @param {Object} options - Constructor options
145
+ * @private
146
+ */
147
+ private _initDatabases;
148
+ /**
149
+ * Get or create a database pool for a given config.
150
+ * @param {DatabaseConfig} config - Database configuration
151
+ * @returns {Pool}
152
+ * @private
153
+ */
154
+ private _getPool;
155
+ /**
156
+ * Checks if cached result is still valid based on TTL.
157
+ * @returns {boolean}
158
+ * @private
159
+ */
160
+ private _isCacheValid;
161
+ /**
162
+ * Tests a single database cluster connectivity.
163
+ * @param {DatabaseConfig} config - Database configuration
164
+ * @returns {Promise<ComponentHealth>}
165
+ * @private
166
+ */
167
+ private _checkSingleDatabase;
168
+ /**
169
+ * Tests all PostgreSQL database clusters in parallel.
170
+ * @returns {Promise<DatabaseClusterHealth | null>}
171
+ * @private
172
+ */
173
+ private _checkAllDatabases;
174
+ /**
175
+ * Tests Redis connectivity using PING command.
176
+ * @returns {Promise<ComponentHealth>}
177
+ * @private
178
+ */
179
+ private _checkRedis;
180
+ /**
181
+ * Performs a full health check on all configured components.
182
+ * Results are cached for the configured TTL to prevent excessive load.
183
+ *
184
+ * @returns {Promise<HealthCheckResult>}
185
+ */
186
+ performHealthCheck(): Promise<HealthCheckResult>;
187
+ /**
188
+ * Clears the cached health check result, forcing the next check to be fresh.
189
+ */
190
+ clearCache(): void;
191
+ /**
192
+ * Express middleware handler for health check endpoint.
193
+ * Returns 200 for healthy/degraded, 503 for unhealthy.
194
+ *
195
+ * @returns {(req: any, res: any) => Promise<void>} Express request handler
196
+ */
197
+ healthHandler(): (req: any, res: any) => Promise<void>;
198
+ /**
199
+ * Register health check endpoint on an Express app.
200
+ *
201
+ * @param {import('express').Application} app - Express application
202
+ * @param {string} [path='/health'] - Path for the health endpoint
203
+ */
204
+ registerHealthEndpoint(app: any, path?: string | undefined): void;
205
+ /**
206
+ * Cleanup resources (database pools).
207
+ * @returns {Promise<void>}
208
+ */
209
+ cleanup(): Promise<void>;
210
+ }
211
+ import { Pool } from "pg";
212
+ //# sourceMappingURL=healthCheckClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthCheckClient.d.ts","sourceRoot":"","sources":["../src/healthCheckClient.js"],"names":[],"mappings":"2BAWa,SAAS,GAAG,WAAW,GAAG,UAAU;;;;;YAKnC,YAAY;;;;;;;;;;;;;;YAOZ,YAAY;;;;;YACL,MAAM,GAAE,eAAe;;;;;;;YAK9B,YAAY;;;;eACZ,MAAM;;;;YACN,OAAO;;;;;YACA,MAAM,GAAE,eAAe,GAAG,qBAAqB;;;;;;;YAKtD,iBAAiB;;;;eACjB,MAAM;;;;;;UAKN,MAAM;;;;SACN,MAAM;;AAlCpB;;GAEG;AAEH;;;;;GAKG;AAEH;;;;GAIG;AAEH;;;;;;GAMG;AAEH;;;;GAIG;AAEH;;;;GAIG;AAEH;;;;;;;;;;;GAWG;AACH;IACE;;;;;;;;OAQG;IACH;QAP4B,WAAW;QACX,YAAY;QACI,sBAAsB;;;QACzC,WAAW,GAAzB,GAAG;QACc,UAAU;QACV,OAAO;OAwBlC;IArBC,iBAA8C;IAC9C,mBAAiE;IACjE,gBACgE;IAEhE,mBAAmD;IAEnD,wCAAwC;IACxC,eADW,kBAAkB,GAAG,IAAI,CACX;IAEzB,gCAAgC;IAChC,gBADW,IAAI,MAAM,EAAE,IAAI,CAAC,CACG;IAE/B,+BAA+B;IAC/B,kBADW,cAAc,EAAE,CACD;IAKxB,qCAA4D;IAIhE;;;;OAIG;IACH,uBAcC;IAED;;;;;OAKG;IACH,iBAaC;IAED;;;;OAIG;IACH,sBAGC;IAED;;;;;OAKG;IACH,6BAiBC;IAED;;;;OAIG;IACH,2BA0BC;IAED;;;;OAIG;IACH,oBA6CC;IAED;;;;;OAKG;IACH,sBAFa,QAAQ,iBAAiB,CAAC,CAuCtC;IAED;;OAEG;IACH,mBAEC;IAED;;;;;OAKG;IACH,uBAFmB,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,CAmBjD;IAED;;;;;OAKG;IACH,kEAGC;IAED;;;OAGG;IACH,WAFa,QAAQ,IAAI,CAAC,CAWzB;CACF"}
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+
3
+ const {
4
+ Pool
5
+ } = require('pg');
6
+ const {
7
+ getRedisClientType,
8
+ REDIS_V4,
9
+ IOREDIS,
10
+ REDIS_V3
11
+ } = require('./redisUtils');
12
+ const HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000;
13
+
14
+ /**
15
+ * @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} ComponentHealth
20
+ * @property {HealthStatus} status - Component health status
21
+ * @property {string} [message] - Optional status message
22
+ * @property {number} [latencyMs] - Connection latency in milliseconds
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} DatabaseClusterHealth
27
+ * @property {HealthStatus} status - Overall databases status
28
+ * @property {Object<string, ComponentHealth>} clusters - Individual cluster health
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} HealthCheckResult
33
+ * @property {HealthStatus} status - Overall health status
34
+ * @property {string} timestamp - ISO timestamp of the check
35
+ * @property {boolean} cached - Whether this result is from cache
36
+ * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} CachedHealthResult
41
+ * @property {HealthCheckResult} result - The cached health check result
42
+ * @property {number} timestamp - Unix timestamp when cached
43
+ */
44
+
45
+ /**
46
+ * @typedef {Object} DatabaseConfig
47
+ * @property {string} name - Database/cluster name
48
+ * @property {string} url - Connection URL
49
+ */
50
+
51
+ /**
52
+ * HealthCheckClient provides a health check middleware for external monitoring services
53
+ * like BetterStack. It validates database and Redis connections with rate limiting
54
+ * to prevent excessive load on backend services.
55
+ *
56
+ * Features:
57
+ * - Multi-cluster DB validation (PostgreSQL)
58
+ * - Redis connection validation (supports ioredis, node-redis v3/v4)
59
+ * - Result caching (default: 60 seconds) to prevent overloading services
60
+ * - Express middleware support
61
+ * - BetterStack-compatible JSON response format
62
+ */
63
+ class HealthCheckClient {
64
+ /**
65
+ * @param {Object} options
66
+ * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL
67
+ * @param {string} [options.databaseName='main'] - Name for the main database
68
+ * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)
69
+ * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)
70
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)
71
+ * @param {string} [options.appName] - Application name for logging
72
+ */
73
+ constructor(options = {}) {
74
+ this.redisClient = options.redisClient || null;
75
+ this.cacheTtlMs = options.cacheTtlMs ?? HEALTH_CHECK_CACHE_TTL_MS;
76
+ this.appName = options.appName || process.env.BUILD_APP_NAME || 'unknown-app';
77
+ this.prefixLogs = `[${this.appName}] [HealthCheck]`;
78
+
79
+ /** @type {CachedHealthResult | null} */
80
+ this._cachedResult = null;
81
+
82
+ /** @type {Map<string, Pool>} */
83
+ this._databasePools = new Map();
84
+
85
+ /** @type {DatabaseConfig[]} */
86
+ this._databaseConfigs = [];
87
+ this._initDatabases(options);
88
+ if (this.redisClient) {
89
+ this._redisClientType = getRedisClientType(this.redisClient);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Initialize database configurations from options.
95
+ * @param {Object} options - Constructor options
96
+ * @private
97
+ */
98
+ _initDatabases(options) {
99
+ const mainUrl = options.databaseUrl || process.env.DATABASE_URL || '';
100
+ const mainName = options.databaseName || 'main';
101
+ if (mainUrl) {
102
+ this._databaseConfigs.push({
103
+ name: mainName,
104
+ url: mainUrl
105
+ });
106
+ }
107
+ const additionalUrls = options.additionalDatabaseUrls || {};
108
+ for (const [name, url] of Object.entries(additionalUrls)) {
109
+ if (url) {
110
+ this._databaseConfigs.push({
111
+ name,
112
+ url
113
+ });
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get or create a database pool for a given config.
120
+ * @param {DatabaseConfig} config - Database configuration
121
+ * @returns {Pool}
122
+ * @private
123
+ */
124
+ _getPool(config) {
125
+ if (!this._databasePools.has(config.name)) {
126
+ this._databasePools.set(config.name, new Pool({
127
+ connectionString: config.url,
128
+ max: 1,
129
+ idleTimeoutMillis: 30000,
130
+ connectionTimeoutMillis: 5000
131
+ }));
132
+ }
133
+ return this._databasePools.get(config.name);
134
+ }
135
+
136
+ /**
137
+ * Checks if cached result is still valid based on TTL.
138
+ * @returns {boolean}
139
+ * @private
140
+ */
141
+ _isCacheValid() {
142
+ if (!this._cachedResult) return false;
143
+ return Date.now() - this._cachedResult.timestamp < this.cacheTtlMs;
144
+ }
145
+
146
+ /**
147
+ * Tests a single database cluster connectivity.
148
+ * @param {DatabaseConfig} config - Database configuration
149
+ * @returns {Promise<ComponentHealth>}
150
+ * @private
151
+ */
152
+ async _checkSingleDatabase(config) {
153
+ const start = Date.now();
154
+ try {
155
+ const pool = this._getPool(config);
156
+ await pool.query('SELECT 1');
157
+ return {
158
+ status: 'healthy',
159
+ latencyMs: Date.now() - start
160
+ };
161
+ } catch (err) {
162
+ return {
163
+ status: 'unhealthy',
164
+ message: err.message,
165
+ latencyMs: Date.now() - start
166
+ };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Tests all PostgreSQL database clusters in parallel.
172
+ * @returns {Promise<DatabaseClusterHealth | null>}
173
+ * @private
174
+ */
175
+ async _checkAllDatabases() {
176
+ if (this._databaseConfigs.length === 0) {
177
+ return null;
178
+ }
179
+ const results = await Promise.all(this._databaseConfigs.map(async config => ({
180
+ name: config.name,
181
+ health: await this._checkSingleDatabase(config)
182
+ })));
183
+ const clusters = {};
184
+ for (const {
185
+ name,
186
+ health
187
+ } of results) {
188
+ clusters[name] = health;
189
+ }
190
+ const statuses = Object.values(clusters).map(c => c.status);
191
+ let overallStatus = 'healthy';
192
+ if (statuses.some(s => s === 'unhealthy')) {
193
+ overallStatus = 'unhealthy';
194
+ } else if (statuses.some(s => s === 'degraded')) {
195
+ overallStatus = 'degraded';
196
+ }
197
+ return {
198
+ status: overallStatus,
199
+ clusters
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Tests Redis connectivity using PING command.
205
+ * @returns {Promise<ComponentHealth>}
206
+ * @private
207
+ */
208
+ async _checkRedis() {
209
+ if (!this.redisClient) {
210
+ return {
211
+ status: 'healthy',
212
+ message: 'Not configured'
213
+ };
214
+ }
215
+ const start = Date.now();
216
+ try {
217
+ let pong;
218
+ if (this._redisClientType === REDIS_V3) {
219
+ pong = await new Promise((resolve, reject) => {
220
+ this.redisClient.ping((err, result) => {
221
+ if (err) reject(err);else resolve(result);
222
+ });
223
+ });
224
+ } else if (this._redisClientType === REDIS_V4 || this._redisClientType === IOREDIS) {
225
+ pong = await this.redisClient.ping();
226
+ } else {
227
+ return {
228
+ status: 'unhealthy',
229
+ message: 'Unknown Redis client type'
230
+ };
231
+ }
232
+ if (pong === 'PONG') {
233
+ return {
234
+ status: 'healthy',
235
+ latencyMs: Date.now() - start
236
+ };
237
+ }
238
+ return {
239
+ status: 'unhealthy',
240
+ message: `Unexpected PING response: ${pong}`,
241
+ latencyMs: Date.now() - start
242
+ };
243
+ } catch (err) {
244
+ return {
245
+ status: 'unhealthy',
246
+ message: err.message,
247
+ latencyMs: Date.now() - start
248
+ };
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Performs a full health check on all configured components.
254
+ * Results are cached for the configured TTL to prevent excessive load.
255
+ *
256
+ * @returns {Promise<HealthCheckResult>}
257
+ */
258
+ async performHealthCheck() {
259
+ if (this._isCacheValid()) {
260
+ return {
261
+ ...this._cachedResult.result,
262
+ cached: true
263
+ };
264
+ }
265
+ const [dbHealth, redisHealth] = await Promise.all([this._checkAllDatabases(), this._checkRedis()]);
266
+ const components = {};
267
+ if (dbHealth) components.database = dbHealth;
268
+ if (this.redisClient) components.redis = redisHealth;
269
+ const statuses = Object.values(components).map(c => c.status);
270
+ let overallStatus = 'healthy';
271
+ if (statuses.some(s => s === 'unhealthy')) {
272
+ overallStatus = 'unhealthy';
273
+ } else if (statuses.some(s => s === 'degraded')) {
274
+ overallStatus = 'degraded';
275
+ }
276
+
277
+ /** @type {HealthCheckResult} */
278
+ const result = {
279
+ status: overallStatus,
280
+ timestamp: new Date().toISOString(),
281
+ cached: false,
282
+ components
283
+ };
284
+ this._cachedResult = {
285
+ result,
286
+ timestamp: Date.now()
287
+ };
288
+ return result;
289
+ }
290
+
291
+ /**
292
+ * Clears the cached health check result, forcing the next check to be fresh.
293
+ */
294
+ clearCache() {
295
+ this._cachedResult = null;
296
+ }
297
+
298
+ /**
299
+ * Express middleware handler for health check endpoint.
300
+ * Returns 200 for healthy/degraded, 503 for unhealthy.
301
+ *
302
+ * @returns {(req: any, res: any) => Promise<void>} Express request handler
303
+ */
304
+ healthHandler() {
305
+ return async (req, res) => {
306
+ try {
307
+ const result = await this.performHealthCheck();
308
+ const statusCode = result.status === 'unhealthy' ? 503 : 200;
309
+ res.status(statusCode).json(result);
310
+ } catch (err) {
311
+ console.error(`${this.prefixLogs} Health check failed:`, err);
312
+ res.status(503).json({
313
+ status: 'unhealthy',
314
+ timestamp: new Date().toISOString(),
315
+ cached: false,
316
+ error: err.message
317
+ });
318
+ }
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Register health check endpoint on an Express app.
324
+ *
325
+ * @param {import('express').Application} app - Express application
326
+ * @param {string} [path='/health'] - Path for the health endpoint
327
+ */
328
+ registerHealthEndpoint(app, path = '/health') {
329
+ app.get(path, this.healthHandler());
330
+ console.info(`${this.prefixLogs} Registered health endpoint at ${path}`);
331
+ }
332
+
333
+ /**
334
+ * Cleanup resources (database pools).
335
+ * @returns {Promise<void>}
336
+ */
337
+ async cleanup() {
338
+ for (const [name, pool] of this._databasePools) {
339
+ try {
340
+ await pool.end();
341
+ } catch (err) {
342
+ console.error(`${this.prefixLogs} Error closing database pool ${name}:`, err);
343
+ }
344
+ }
345
+ this._databasePools.clear();
346
+ }
347
+ }
348
+ module.exports = {
349
+ HealthCheckClient
350
+ };
351
+ //# sourceMappingURL=healthCheckClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthCheckClient.js","names":["Pool","require","getRedisClientType","REDIS_V4","IOREDIS","REDIS_V3","HEALTH_CHECK_CACHE_TTL_MS","HealthCheckClient","constructor","options","redisClient","cacheTtlMs","appName","process","env","BUILD_APP_NAME","prefixLogs","_cachedResult","_databasePools","Map","_databaseConfigs","_initDatabases","_redisClientType","mainUrl","databaseUrl","DATABASE_URL","mainName","databaseName","push","name","url","additionalUrls","additionalDatabaseUrls","Object","entries","_getPool","config","has","set","connectionString","max","idleTimeoutMillis","connectionTimeoutMillis","get","_isCacheValid","Date","now","timestamp","_checkSingleDatabase","start","pool","query","status","latencyMs","err","message","_checkAllDatabases","length","results","Promise","all","map","health","clusters","statuses","values","c","overallStatus","some","s","_checkRedis","pong","resolve","reject","ping","result","performHealthCheck","cached","dbHealth","redisHealth","components","database","redis","toISOString","clearCache","healthHandler","req","res","statusCode","json","console","error","registerHealthEndpoint","app","path","info","cleanup","end","clear","module","exports"],"sources":["../src/healthCheckClient.js"],"sourcesContent":["const { Pool } = require('pg')\nconst {\n getRedisClientType,\n REDIS_V4,\n IOREDIS,\n REDIS_V3,\n} = require('./redisUtils')\n\nconst HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000\n\n/**\n * @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus\n */\n\n/**\n * @typedef {Object} ComponentHealth\n * @property {HealthStatus} status - Component health status\n * @property {string} [message] - Optional status message\n * @property {number} [latencyMs] - Connection latency in milliseconds\n */\n\n/**\n * @typedef {Object} DatabaseClusterHealth\n * @property {HealthStatus} status - Overall databases status\n * @property {Object<string, ComponentHealth>} clusters - Individual cluster health\n */\n\n/**\n * @typedef {Object} HealthCheckResult\n * @property {HealthStatus} status - Overall health status\n * @property {string} timestamp - ISO timestamp of the check\n * @property {boolean} cached - Whether this result is from cache\n * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health\n */\n\n/**\n * @typedef {Object} CachedHealthResult\n * @property {HealthCheckResult} result - The cached health check result\n * @property {number} timestamp - Unix timestamp when cached\n */\n\n/**\n * @typedef {Object} DatabaseConfig\n * @property {string} name - Database/cluster name\n * @property {string} url - Connection URL\n */\n\n/**\n * HealthCheckClient provides a health check middleware for external monitoring services\n * like BetterStack. It validates database and Redis connections with rate limiting\n * to prevent excessive load on backend services.\n *\n * Features:\n * - Multi-cluster DB validation (PostgreSQL)\n * - Redis connection validation (supports ioredis, node-redis v3/v4)\n * - Result caching (default: 60 seconds) to prevent overloading services\n * - Express middleware support\n * - BetterStack-compatible JSON response format\n */\nclass HealthCheckClient {\n /**\n * @param {Object} options\n * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL\n * @param {string} [options.databaseName='main'] - Name for the main database\n * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)\n * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)\n * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)\n * @param {string} [options.appName] - Application name for logging\n */\n constructor(options = {}) {\n this.redisClient = options.redisClient || null\n this.cacheTtlMs = options.cacheTtlMs ?? HEALTH_CHECK_CACHE_TTL_MS\n this.appName =\n options.appName || process.env.BUILD_APP_NAME || 'unknown-app'\n\n this.prefixLogs = `[${this.appName}] [HealthCheck]`\n\n /** @type {CachedHealthResult | null} */\n this._cachedResult = null\n\n /** @type {Map<string, Pool>} */\n this._databasePools = new Map()\n\n /** @type {DatabaseConfig[]} */\n this._databaseConfigs = []\n\n this._initDatabases(options)\n\n if (this.redisClient) {\n this._redisClientType = getRedisClientType(this.redisClient)\n }\n }\n\n /**\n * Initialize database configurations from options.\n * @param {Object} options - Constructor options\n * @private\n */\n _initDatabases(options) {\n const mainUrl = options.databaseUrl || process.env.DATABASE_URL || ''\n const mainName = options.databaseName || 'main'\n\n if (mainUrl) {\n this._databaseConfigs.push({ name: mainName, url: mainUrl })\n }\n\n const additionalUrls = options.additionalDatabaseUrls || {}\n for (const [name, url] of Object.entries(additionalUrls)) {\n if (url) {\n this._databaseConfigs.push({ name, url })\n }\n }\n }\n\n /**\n * Get or create a database pool for a given config.\n * @param {DatabaseConfig} config - Database configuration\n * @returns {Pool}\n * @private\n */\n _getPool(config) {\n if (!this._databasePools.has(config.name)) {\n this._databasePools.set(\n config.name,\n new Pool({\n connectionString: config.url,\n max: 1,\n idleTimeoutMillis: 30000,\n connectionTimeoutMillis: 5000,\n })\n )\n }\n return this._databasePools.get(config.name)\n }\n\n /**\n * Checks if cached result is still valid based on TTL.\n * @returns {boolean}\n * @private\n */\n _isCacheValid() {\n if (!this._cachedResult) return false\n return Date.now() - this._cachedResult.timestamp < this.cacheTtlMs\n }\n\n /**\n * Tests a single database cluster connectivity.\n * @param {DatabaseConfig} config - Database configuration\n * @returns {Promise<ComponentHealth>}\n * @private\n */\n async _checkSingleDatabase(config) {\n const start = Date.now()\n\n try {\n const pool = this._getPool(config)\n await pool.query('SELECT 1')\n return {\n status: 'healthy',\n latencyMs: Date.now() - start,\n }\n } catch (err) {\n return {\n status: 'unhealthy',\n message: err.message,\n latencyMs: Date.now() - start,\n }\n }\n }\n\n /**\n * Tests all PostgreSQL database clusters in parallel.\n * @returns {Promise<DatabaseClusterHealth | null>}\n * @private\n */\n async _checkAllDatabases() {\n if (this._databaseConfigs.length === 0) {\n return null\n }\n\n const results = await Promise.all(\n this._databaseConfigs.map(async config => ({\n name: config.name,\n health: await this._checkSingleDatabase(config),\n }))\n )\n\n const clusters = {}\n for (const { name, health } of results) {\n clusters[name] = health\n }\n\n const statuses = Object.values(clusters).map(c => c.status)\n let overallStatus = 'healthy'\n if (statuses.some(s => s === 'unhealthy')) {\n overallStatus = 'unhealthy'\n } else if (statuses.some(s => s === 'degraded')) {\n overallStatus = 'degraded'\n }\n\n return { status: overallStatus, clusters }\n }\n\n /**\n * Tests Redis connectivity using PING command.\n * @returns {Promise<ComponentHealth>}\n * @private\n */\n async _checkRedis() {\n if (!this.redisClient) {\n return { status: 'healthy', message: 'Not configured' }\n }\n\n const start = Date.now()\n\n try {\n let pong\n\n if (this._redisClientType === REDIS_V3) {\n pong = await new Promise((resolve, reject) => {\n this.redisClient.ping((err, result) => {\n if (err) reject(err)\n else resolve(result)\n })\n })\n } else if (\n this._redisClientType === REDIS_V4 ||\n this._redisClientType === IOREDIS\n ) {\n pong = await this.redisClient.ping()\n } else {\n return { status: 'unhealthy', message: 'Unknown Redis client type' }\n }\n\n if (pong === 'PONG') {\n return {\n status: 'healthy',\n latencyMs: Date.now() - start,\n }\n }\n\n return {\n status: 'unhealthy',\n message: `Unexpected PING response: ${pong}`,\n latencyMs: Date.now() - start,\n }\n } catch (err) {\n return {\n status: 'unhealthy',\n message: err.message,\n latencyMs: Date.now() - start,\n }\n }\n }\n\n /**\n * Performs a full health check on all configured components.\n * Results are cached for the configured TTL to prevent excessive load.\n *\n * @returns {Promise<HealthCheckResult>}\n */\n async performHealthCheck() {\n if (this._isCacheValid()) {\n return { ...this._cachedResult.result, cached: true }\n }\n\n const [dbHealth, redisHealth] = await Promise.all([\n this._checkAllDatabases(),\n this._checkRedis(),\n ])\n\n const components = {}\n if (dbHealth) components.database = dbHealth\n if (this.redisClient) components.redis = redisHealth\n\n const statuses = Object.values(components).map(c => c.status)\n let overallStatus = 'healthy'\n\n if (statuses.some(s => s === 'unhealthy')) {\n overallStatus = 'unhealthy'\n } else if (statuses.some(s => s === 'degraded')) {\n overallStatus = 'degraded'\n }\n\n /** @type {HealthCheckResult} */\n const result = {\n status: overallStatus,\n timestamp: new Date().toISOString(),\n cached: false,\n components,\n }\n\n this._cachedResult = {\n result,\n timestamp: Date.now(),\n }\n\n return result\n }\n\n /**\n * Clears the cached health check result, forcing the next check to be fresh.\n */\n clearCache() {\n this._cachedResult = null\n }\n\n /**\n * Express middleware handler for health check endpoint.\n * Returns 200 for healthy/degraded, 503 for unhealthy.\n *\n * @returns {(req: any, res: any) => Promise<void>} Express request handler\n */\n healthHandler() {\n return async (req, res) => {\n try {\n const result = await this.performHealthCheck()\n const statusCode = result.status === 'unhealthy' ? 503 : 200\n\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: 'unhealthy',\n timestamp: new Date().toISOString(),\n cached: false,\n error: err.message,\n })\n }\n }\n }\n\n /**\n * Register health check endpoint on an Express app.\n *\n * @param {import('express').Application} app - Express application\n * @param {string} [path='/health'] - Path for the health endpoint\n */\n registerHealthEndpoint(app, path = '/health') {\n app.get(path, this.healthHandler())\n console.info(`${this.prefixLogs} Registered health endpoint at ${path}`)\n }\n\n /**\n * Cleanup resources (database pools).\n * @returns {Promise<void>}\n */\n async cleanup() {\n for (const [name, pool] of this._databasePools) {\n try {\n await pool.end()\n } catch (err) {\n console.error(`${this.prefixLogs} Error closing database pool ${name}:`, err)\n }\n }\n this._databasePools.clear()\n }\n}\n\nmodule.exports = { HealthCheckClient }\n"],"mappings":"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","ignoreList":[]}
package/lib/index.d.ts CHANGED
@@ -3,5 +3,6 @@ export * from './metricsClient';
3
3
  export * from './metricsRedisClient';
4
4
  export * from './metricsQueueRedisClient';
5
5
  export * from './metricsDatabaseClient';
6
+ export * from './healthCheckClient';
6
7
  export * from './redisUtils';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,cAAc,CAAA"}
package/lib/index.js CHANGED
@@ -58,6 +58,17 @@ Object.keys(_metricsDatabaseClient).forEach(function (key) {
58
58
  }
59
59
  });
60
60
  });
61
+ var _healthCheckClient = require("./healthCheckClient");
62
+ Object.keys(_healthCheckClient).forEach(function (key) {
63
+ if (key === "default" || key === "__esModule") return;
64
+ if (key in exports && exports[key] === _healthCheckClient[key]) return;
65
+ Object.defineProperty(exports, key, {
66
+ enumerable: true,
67
+ get: function () {
68
+ return _healthCheckClient[key];
69
+ }
70
+ });
71
+ });
61
72
  var _redisUtils = require("./redisUtils");
62
73
  Object.keys(_redisUtils).forEach(function (key) {
63
74
  if (key === "default" || key === "__esModule") return;
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["_baseMetricsClient","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get","_metricsClient","_metricsRedisClient","_metricsQueueRedisClient","_metricsDatabaseClient","_redisUtils"],"sources":["../src/index.ts"],"sourcesContent":["export * from './baseMetricsClient'\nexport * from './metricsClient'\nexport * from './metricsRedisClient'\nexport * from './metricsQueueRedisClient'\nexport * from './metricsDatabaseClient'\nexport * from './redisUtils'\n"],"mappings":";;;;;AAAA,IAAAA,kBAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,kBAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,kBAAA,CAAAK,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAT,kBAAA,CAAAK,GAAA;IAAA;EAAA;AAAA;AACA,IAAAK,cAAA,GAAAT,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAO,cAAA,EAAAN,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAK,cAAA,CAAAL,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAC,cAAA,CAAAL,GAAA;IAAA;EAAA;AAAA;AACA,IAAAM,mBAAA,GAAAV,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAQ,mBAAA,EAAAP,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAM,mBAAA,CAAAN,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAE,mBAAA,CAAAN,GAAA;IAAA;EAAA;AAAA;AACA,IAAAO,wBAAA,GAAAX,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAS,wBAAA,EAAAR,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAO,wBAAA,CAAAP,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAG,wBAAA,CAAAP,GAAA;IAAA;EAAA;AAAA;AACA,IAAAQ,sBAAA,GAAAZ,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAU,sBAAA,EAAAT,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAQ,sBAAA,CAAAR,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAI,sBAAA,CAAAR,GAAA;IAAA;EAAA;AAAA;AACA,IAAAS,WAAA,GAAAb,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAW,WAAA,EAAAV,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAS,WAAA,CAAAT,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAK,WAAA,CAAAT,GAAA;IAAA;EAAA;AAAA","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["_baseMetricsClient","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get","_metricsClient","_metricsRedisClient","_metricsQueueRedisClient","_metricsDatabaseClient","_healthCheckClient","_redisUtils"],"sources":["../src/index.ts"],"sourcesContent":["export * from './baseMetricsClient'\nexport * from './metricsClient'\nexport * from './metricsRedisClient'\nexport * from './metricsQueueRedisClient'\nexport * from './metricsDatabaseClient'\nexport * from './healthCheckClient'\nexport * from './redisUtils'\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.119",
3
+ "version": "0.1.120",
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",
@@ -0,0 +1,360 @@
1
+ const { Pool } = require('pg')
2
+ const {
3
+ getRedisClientType,
4
+ REDIS_V4,
5
+ IOREDIS,
6
+ REDIS_V3,
7
+ } = require('./redisUtils')
8
+
9
+ const HEALTH_CHECK_CACHE_TTL_MS = 60 * 1000
10
+
11
+ /**
12
+ * @typedef {'healthy' | 'unhealthy' | 'degraded'} HealthStatus
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} ComponentHealth
17
+ * @property {HealthStatus} status - Component health status
18
+ * @property {string} [message] - Optional status message
19
+ * @property {number} [latencyMs] - Connection latency in milliseconds
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} DatabaseClusterHealth
24
+ * @property {HealthStatus} status - Overall databases status
25
+ * @property {Object<string, ComponentHealth>} clusters - Individual cluster health
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} HealthCheckResult
30
+ * @property {HealthStatus} status - Overall health status
31
+ * @property {string} timestamp - ISO timestamp of the check
32
+ * @property {boolean} cached - Whether this result is from cache
33
+ * @property {Object<string, ComponentHealth | DatabaseClusterHealth>} components - Individual component health
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} CachedHealthResult
38
+ * @property {HealthCheckResult} result - The cached health check result
39
+ * @property {number} timestamp - Unix timestamp when cached
40
+ */
41
+
42
+ /**
43
+ * @typedef {Object} DatabaseConfig
44
+ * @property {string} name - Database/cluster name
45
+ * @property {string} url - Connection URL
46
+ */
47
+
48
+ /**
49
+ * HealthCheckClient provides a health check middleware for external monitoring services
50
+ * like BetterStack. It validates database and Redis connections with rate limiting
51
+ * to prevent excessive load on backend services.
52
+ *
53
+ * Features:
54
+ * - Multi-cluster DB validation (PostgreSQL)
55
+ * - Redis connection validation (supports ioredis, node-redis v3/v4)
56
+ * - Result caching (default: 60 seconds) to prevent overloading services
57
+ * - Express middleware support
58
+ * - BetterStack-compatible JSON response format
59
+ */
60
+ class HealthCheckClient {
61
+ /**
62
+ * @param {Object} options
63
+ * @param {string} [options.databaseUrl] - Main PostgreSQL connection URL
64
+ * @param {string} [options.databaseName='main'] - Name for the main database
65
+ * @param {Object<string, string>} [options.additionalDatabaseUrls] - Additional DB clusters (name -> URL)
66
+ * @param {any} [options.redisClient] - Redis client instance (ioredis or node-redis)
67
+ * @param {number} [options.cacheTtlMs=60000] - Cache TTL in milliseconds (default: 60s)
68
+ * @param {string} [options.appName] - Application name for logging
69
+ */
70
+ constructor(options = {}) {
71
+ this.redisClient = options.redisClient || null
72
+ this.cacheTtlMs = options.cacheTtlMs ?? HEALTH_CHECK_CACHE_TTL_MS
73
+ this.appName =
74
+ options.appName || process.env.BUILD_APP_NAME || 'unknown-app'
75
+
76
+ this.prefixLogs = `[${this.appName}] [HealthCheck]`
77
+
78
+ /** @type {CachedHealthResult | null} */
79
+ this._cachedResult = null
80
+
81
+ /** @type {Map<string, Pool>} */
82
+ this._databasePools = new Map()
83
+
84
+ /** @type {DatabaseConfig[]} */
85
+ this._databaseConfigs = []
86
+
87
+ this._initDatabases(options)
88
+
89
+ if (this.redisClient) {
90
+ this._redisClientType = getRedisClientType(this.redisClient)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Initialize database configurations from options.
96
+ * @param {Object} options - Constructor options
97
+ * @private
98
+ */
99
+ _initDatabases(options) {
100
+ const mainUrl = options.databaseUrl || process.env.DATABASE_URL || ''
101
+ const mainName = options.databaseName || 'main'
102
+
103
+ if (mainUrl) {
104
+ this._databaseConfigs.push({ name: mainName, url: mainUrl })
105
+ }
106
+
107
+ const additionalUrls = options.additionalDatabaseUrls || {}
108
+ for (const [name, url] of Object.entries(additionalUrls)) {
109
+ if (url) {
110
+ this._databaseConfigs.push({ name, url })
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get or create a database pool for a given config.
117
+ * @param {DatabaseConfig} config - Database configuration
118
+ * @returns {Pool}
119
+ * @private
120
+ */
121
+ _getPool(config) {
122
+ if (!this._databasePools.has(config.name)) {
123
+ this._databasePools.set(
124
+ config.name,
125
+ new Pool({
126
+ connectionString: config.url,
127
+ max: 1,
128
+ idleTimeoutMillis: 30000,
129
+ connectionTimeoutMillis: 5000,
130
+ })
131
+ )
132
+ }
133
+ return this._databasePools.get(config.name)
134
+ }
135
+
136
+ /**
137
+ * Checks if cached result is still valid based on TTL.
138
+ * @returns {boolean}
139
+ * @private
140
+ */
141
+ _isCacheValid() {
142
+ if (!this._cachedResult) return false
143
+ return Date.now() - this._cachedResult.timestamp < this.cacheTtlMs
144
+ }
145
+
146
+ /**
147
+ * Tests a single database cluster connectivity.
148
+ * @param {DatabaseConfig} config - Database configuration
149
+ * @returns {Promise<ComponentHealth>}
150
+ * @private
151
+ */
152
+ async _checkSingleDatabase(config) {
153
+ const start = Date.now()
154
+
155
+ try {
156
+ const pool = this._getPool(config)
157
+ await pool.query('SELECT 1')
158
+ return {
159
+ status: 'healthy',
160
+ latencyMs: Date.now() - start,
161
+ }
162
+ } catch (err) {
163
+ return {
164
+ status: 'unhealthy',
165
+ message: err.message,
166
+ latencyMs: Date.now() - start,
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Tests all PostgreSQL database clusters in parallel.
173
+ * @returns {Promise<DatabaseClusterHealth | null>}
174
+ * @private
175
+ */
176
+ async _checkAllDatabases() {
177
+ if (this._databaseConfigs.length === 0) {
178
+ return null
179
+ }
180
+
181
+ const results = await Promise.all(
182
+ this._databaseConfigs.map(async config => ({
183
+ name: config.name,
184
+ health: await this._checkSingleDatabase(config),
185
+ }))
186
+ )
187
+
188
+ const clusters = {}
189
+ for (const { name, health } of results) {
190
+ clusters[name] = health
191
+ }
192
+
193
+ const statuses = Object.values(clusters).map(c => c.status)
194
+ let overallStatus = 'healthy'
195
+ if (statuses.some(s => s === 'unhealthy')) {
196
+ overallStatus = 'unhealthy'
197
+ } else if (statuses.some(s => s === 'degraded')) {
198
+ overallStatus = 'degraded'
199
+ }
200
+
201
+ return { status: overallStatus, clusters }
202
+ }
203
+
204
+ /**
205
+ * Tests Redis connectivity using PING command.
206
+ * @returns {Promise<ComponentHealth>}
207
+ * @private
208
+ */
209
+ async _checkRedis() {
210
+ if (!this.redisClient) {
211
+ return { status: 'healthy', message: 'Not configured' }
212
+ }
213
+
214
+ const start = Date.now()
215
+
216
+ try {
217
+ let pong
218
+
219
+ if (this._redisClientType === REDIS_V3) {
220
+ pong = await new Promise((resolve, reject) => {
221
+ this.redisClient.ping((err, result) => {
222
+ if (err) reject(err)
223
+ else resolve(result)
224
+ })
225
+ })
226
+ } else if (
227
+ this._redisClientType === REDIS_V4 ||
228
+ this._redisClientType === IOREDIS
229
+ ) {
230
+ pong = await this.redisClient.ping()
231
+ } else {
232
+ return { status: 'unhealthy', message: 'Unknown Redis client type' }
233
+ }
234
+
235
+ if (pong === 'PONG') {
236
+ return {
237
+ status: 'healthy',
238
+ latencyMs: Date.now() - start,
239
+ }
240
+ }
241
+
242
+ return {
243
+ status: 'unhealthy',
244
+ message: `Unexpected PING response: ${pong}`,
245
+ latencyMs: Date.now() - start,
246
+ }
247
+ } catch (err) {
248
+ return {
249
+ status: 'unhealthy',
250
+ message: err.message,
251
+ latencyMs: Date.now() - start,
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Performs a full health check on all configured components.
258
+ * Results are cached for the configured TTL to prevent excessive load.
259
+ *
260
+ * @returns {Promise<HealthCheckResult>}
261
+ */
262
+ async performHealthCheck() {
263
+ if (this._isCacheValid()) {
264
+ return { ...this._cachedResult.result, cached: true }
265
+ }
266
+
267
+ const [dbHealth, redisHealth] = await Promise.all([
268
+ this._checkAllDatabases(),
269
+ this._checkRedis(),
270
+ ])
271
+
272
+ const components = {}
273
+ if (dbHealth) components.database = dbHealth
274
+ if (this.redisClient) components.redis = redisHealth
275
+
276
+ const statuses = Object.values(components).map(c => c.status)
277
+ let overallStatus = 'healthy'
278
+
279
+ if (statuses.some(s => s === 'unhealthy')) {
280
+ overallStatus = 'unhealthy'
281
+ } else if (statuses.some(s => s === 'degraded')) {
282
+ overallStatus = 'degraded'
283
+ }
284
+
285
+ /** @type {HealthCheckResult} */
286
+ const result = {
287
+ status: overallStatus,
288
+ timestamp: new Date().toISOString(),
289
+ cached: false,
290
+ components,
291
+ }
292
+
293
+ this._cachedResult = {
294
+ result,
295
+ timestamp: Date.now(),
296
+ }
297
+
298
+ return result
299
+ }
300
+
301
+ /**
302
+ * Clears the cached health check result, forcing the next check to be fresh.
303
+ */
304
+ clearCache() {
305
+ this._cachedResult = null
306
+ }
307
+
308
+ /**
309
+ * Express middleware handler for health check endpoint.
310
+ * Returns 200 for healthy/degraded, 503 for unhealthy.
311
+ *
312
+ * @returns {(req: any, res: any) => Promise<void>} Express request handler
313
+ */
314
+ healthHandler() {
315
+ return async (req, res) => {
316
+ try {
317
+ const result = await this.performHealthCheck()
318
+ const statusCode = result.status === 'unhealthy' ? 503 : 200
319
+
320
+ res.status(statusCode).json(result)
321
+ } catch (err) {
322
+ console.error(`${this.prefixLogs} Health check failed:`, err)
323
+ res.status(503).json({
324
+ status: 'unhealthy',
325
+ timestamp: new Date().toISOString(),
326
+ cached: false,
327
+ error: err.message,
328
+ })
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Register health check endpoint on an Express app.
335
+ *
336
+ * @param {import('express').Application} app - Express application
337
+ * @param {string} [path='/health'] - Path for the health endpoint
338
+ */
339
+ registerHealthEndpoint(app, path = '/health') {
340
+ app.get(path, this.healthHandler())
341
+ console.info(`${this.prefixLogs} Registered health endpoint at ${path}`)
342
+ }
343
+
344
+ /**
345
+ * Cleanup resources (database pools).
346
+ * @returns {Promise<void>}
347
+ */
348
+ async cleanup() {
349
+ for (const [name, pool] of this._databasePools) {
350
+ try {
351
+ await pool.end()
352
+ } catch (err) {
353
+ console.error(`${this.prefixLogs} Error closing database pool ${name}:`, err)
354
+ }
355
+ }
356
+ this._databasePools.clear()
357
+ }
358
+ }
359
+
360
+ module.exports = { HealthCheckClient }
package/src/index.ts CHANGED
@@ -3,4 +3,5 @@ export * from './metricsClient'
3
3
  export * from './metricsRedisClient'
4
4
  export * from './metricsQueueRedisClient'
5
5
  export * from './metricsDatabaseClient'
6
+ export * from './healthCheckClient'
6
7
  export * from './redisUtils'