@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 +11 -8
- package/README.md +54 -0
- package/lib/healthCheckClient.d.ts +212 -0
- package/lib/healthCheckClient.d.ts.map +1 -0
- package/lib/healthCheckClient.js +351 -0
- package/lib/healthCheckClient.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/healthCheckClient.js +360 -0
- package/src/index.ts +1 -0
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
package/lib/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -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 }
|