@adalo/metrics 0.0.0-staging.1
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 +14 -0
- package/.eslintignore +3 -0
- package/.eslintrc +61 -0
- package/.github/pull_request_template.md +14 -0
- package/.github/workflows/code-style.yml +29 -0
- package/.github/workflows/deploy-staging.yml +34 -0
- package/.github/workflows/deploy.yml +29 -0
- package/.github/workflows/tests.yml +17 -0
- package/.idea/codeStyles/Project.xml +101 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/git_toolbox_prj.xml +15 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierrc +10 -0
- package/README-health.md +234 -0
- package/README.md +120 -0
- package/__tests__/metricsRedisClient.test.js +138 -0
- package/babel.config.js +20 -0
- package/lib/health/databaseChecker.d.ts +43 -0
- package/lib/health/databaseChecker.d.ts.map +1 -0
- package/lib/health/databaseChecker.js +189 -0
- package/lib/health/databaseChecker.js.map +1 -0
- package/lib/health/healthCheckCache.d.ts +59 -0
- package/lib/health/healthCheckCache.d.ts.map +1 -0
- package/lib/health/healthCheckCache.js +187 -0
- package/lib/health/healthCheckCache.js.map +1 -0
- package/lib/health/healthCheckClient.d.ts +124 -0
- package/lib/health/healthCheckClient.d.ts.map +1 -0
- package/lib/health/healthCheckClient.js +324 -0
- package/lib/health/healthCheckClient.js.map +1 -0
- package/lib/health/healthCheckUtils.d.ts +52 -0
- package/lib/health/healthCheckUtils.d.ts.map +1 -0
- package/lib/health/healthCheckUtils.js +129 -0
- package/lib/health/healthCheckUtils.js.map +1 -0
- package/lib/health/healthCheckWorker.d.ts +2 -0
- package/lib/health/healthCheckWorker.d.ts.map +1 -0
- package/lib/health/healthCheckWorker.js +70 -0
- package/lib/health/healthCheckWorker.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +105 -0
- package/lib/index.js.map +1 -0
- package/lib/metrics/baseMetricsClient.d.ts +174 -0
- package/lib/metrics/baseMetricsClient.d.ts.map +1 -0
- package/lib/metrics/baseMetricsClient.js +428 -0
- package/lib/metrics/baseMetricsClient.js.map +1 -0
- package/lib/metrics/metricsClient.d.ts +95 -0
- package/lib/metrics/metricsClient.d.ts.map +1 -0
- package/lib/metrics/metricsClient.js +239 -0
- package/lib/metrics/metricsClient.js.map +1 -0
- package/lib/metrics/metricsDatabaseClient.d.ts +74 -0
- package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -0
- package/lib/metrics/metricsDatabaseClient.js +218 -0
- package/lib/metrics/metricsDatabaseClient.js.map +1 -0
- package/lib/metrics/metricsQueueRedisClient.d.ts +57 -0
- package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -0
- package/lib/metrics/metricsQueueRedisClient.js +277 -0
- package/lib/metrics/metricsQueueRedisClient.js.map +1 -0
- package/lib/metrics/metricsRedisClient.d.ts +71 -0
- package/lib/metrics/metricsRedisClient.d.ts.map +1 -0
- package/lib/metrics/metricsRedisClient.js +370 -0
- package/lib/metrics/metricsRedisClient.js.map +1 -0
- package/lib/redisUtils.d.ts +53 -0
- package/lib/redisUtils.d.ts.map +1 -0
- package/lib/redisUtils.js +140 -0
- package/lib/redisUtils.js.map +1 -0
- package/package.json +66 -0
- package/scripts/README.md +43 -0
- package/scripts/clearMetrics.js +6 -0
- package/src/health/databaseChecker.js +183 -0
- package/src/health/healthCheckCache.js +216 -0
- package/src/health/healthCheckClient.js +347 -0
- package/src/health/healthCheckUtils.js +125 -0
- package/src/health/healthCheckWorker.js +71 -0
- package/src/index.ts +9 -0
- package/src/metrics/baseMetricsClient.js +494 -0
- package/src/metrics/metricsClient.js +284 -0
- package/src/metrics/metricsDatabaseClient.js +236 -0
- package/src/metrics/metricsQueueRedisClient.js +352 -0
- package/src/metrics/metricsRedisClient.js +417 -0
- package/src/redisUtils.js +155 -0
- package/tsconfig.json +19 -0
- package/tsconfig.types.json +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adalo/metrics",
|
|
3
|
+
"version": "0.0.0-staging.1",
|
|
4
|
+
"description": "Reusable metrics utilities for Node.js and Laravel apps",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "babel src --out-dir lib --source-maps --extensions .js,.ts && yarn build:types",
|
|
9
|
+
"build:types": "tsc --project ./tsconfig.types.json",
|
|
10
|
+
"watch": "yarn build -- --watch",
|
|
11
|
+
"start": "yarn watch",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test:watch": "yarn test --watch",
|
|
14
|
+
"test:all": "yarn format:check && yarn test",
|
|
15
|
+
"types:check": "tsc --project ./tsconfig.json",
|
|
16
|
+
"prettier": "prettier --write 'src/**'",
|
|
17
|
+
"prettier:check": "prettier --check 'src/**'",
|
|
18
|
+
"lint": "eslint --fix src --ext .js,.ts",
|
|
19
|
+
"lint:check": "eslint src --ext .js,.ts",
|
|
20
|
+
"format": "yarn prettier && yarn lint",
|
|
21
|
+
"format:check": "yarn prettier:check && yarn lint:check"
|
|
22
|
+
},
|
|
23
|
+
"author": "@AdaloHQ",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"bee-queue": "^1.2.2",
|
|
27
|
+
"dotenv": "^8.2.0",
|
|
28
|
+
"mysql2": "^3.11.0",
|
|
29
|
+
"pg": "^8.16.3",
|
|
30
|
+
"prom-client": "^15.1.3",
|
|
31
|
+
"@types/pg": "^8.15.6"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@babel/cli": "^7.24.0",
|
|
35
|
+
"@babel/core": "^7.24.0",
|
|
36
|
+
"@babel/preset-env": "^7.24.0",
|
|
37
|
+
"@babel/preset-typescript": "^7.24.0",
|
|
38
|
+
"@types/ioredis": "^5.0.0",
|
|
39
|
+
"@types/jest": "28.1.6",
|
|
40
|
+
"@types/node": "18.0.4",
|
|
41
|
+
"@types/redis": "2",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "5.30.6",
|
|
43
|
+
"@typescript-eslint/parser": "5.30.6",
|
|
44
|
+
"babel-jest": "28.1.3",
|
|
45
|
+
"eslint": "~8.4.1",
|
|
46
|
+
"eslint-config-airbnb-base": "~15.0.0",
|
|
47
|
+
"eslint-config-airbnb-typescript": "17.0.0",
|
|
48
|
+
"eslint-config-prettier": "~8.3.0",
|
|
49
|
+
"eslint-import-resolver-typescript": "2.7.1",
|
|
50
|
+
"eslint-plugin-import": "~2.25.3",
|
|
51
|
+
"jest": "28.1.3",
|
|
52
|
+
"prettier": "2.5.1",
|
|
53
|
+
"typescript": "4.7.4"
|
|
54
|
+
},
|
|
55
|
+
"resolutions": {
|
|
56
|
+
"@types/istanbul-lib-report": "^3.0.0"
|
|
57
|
+
},
|
|
58
|
+
"jest": {
|
|
59
|
+
"testMatch": [
|
|
60
|
+
"**/__tests__/**/*.[jt]s?(x)"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=13.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# This is utils for manage metrics
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Type envs to handling require metrics
|
|
5
|
+
```js
|
|
6
|
+
BUILD_APP_NAME=
|
|
7
|
+
HOSTNAME=
|
|
8
|
+
BUILD_DYNO_PROCESS_TYPE=
|
|
9
|
+
METRICS_ENABLED=
|
|
10
|
+
METRICS_LOG_VALUES=
|
|
11
|
+
METRICS_PUSHGATEWAY_URL=
|
|
12
|
+
METRICS_PUSHGATEWAY_SECRET=
|
|
13
|
+
METRICS_INTERVAL_SEC=
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Clear metriks for service
|
|
19
|
+
```
|
|
20
|
+
node scripts/clearMetrics.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Publish metrics to VM-agent (example)
|
|
24
|
+
|
|
25
|
+
Publishes built-in gauges (CPU, memory, uptime, etc.) and an example custom gauge to VM-agent.
|
|
26
|
+
|
|
27
|
+
1. Set env (or `.env`):
|
|
28
|
+
- `METRICS_PUSHGATEWAY_URL` – VM-agent import URL, e.g. `https://vm-agent.staging.infradalogs.adalo.com/api/v1/import/prometheus`
|
|
29
|
+
- `METRICS_PUSHGATEWAY_SECRET` – Base64 of `user:password` (same as VM-agent BASIC_USER / BASIC_PASSWORD)
|
|
30
|
+
- `METRICS_ENABLED=true`
|
|
31
|
+
- `METRICS_INTERVAL_SEC=15` (optional, default 15)
|
|
32
|
+
|
|
33
|
+
2. Generate secret:
|
|
34
|
+
```bash
|
|
35
|
+
echo -n 'user:password' | base64
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. Run:
|
|
39
|
+
```bash
|
|
40
|
+
node scripts/publish-metrics-example.js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Metrics appear in VM-agent (and VictoriaMetrics) under names like `app_process_cpu_usage_percent`, `app_uptime_seconds`, `app_example_custom_total`, etc.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const { Pool } = require('pg')
|
|
2
|
+
const mysql = require('mysql2/promise')
|
|
3
|
+
|
|
4
|
+
const DB_TYPE_POSTGRES = 'postgres'
|
|
5
|
+
const DB_TYPE_MYSQL = 'mysql'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @returns {bigint}
|
|
9
|
+
*/
|
|
10
|
+
function nowNs() {
|
|
11
|
+
return process.hrtime.bigint()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {bigint} deltaNs
|
|
16
|
+
* @returns {number}
|
|
17
|
+
*/
|
|
18
|
+
function nsToMs(deltaNs) {
|
|
19
|
+
return Number(deltaNs) / 1_000_000
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} url
|
|
24
|
+
* @returns {string} postgres | mysql
|
|
25
|
+
*/
|
|
26
|
+
function getDatabaseType(url) {
|
|
27
|
+
if (!url || typeof url !== 'string') return DB_TYPE_POSTGRES
|
|
28
|
+
const lower = url.toLowerCase()
|
|
29
|
+
if (lower.startsWith('mysql://') || lower.startsWith('mysql2://')) {
|
|
30
|
+
return DB_TYPE_MYSQL
|
|
31
|
+
}
|
|
32
|
+
if (lower.startsWith('mariadb://')) {
|
|
33
|
+
return DB_TYPE_MYSQL
|
|
34
|
+
}
|
|
35
|
+
return DB_TYPE_POSTGRES
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} url
|
|
40
|
+
* @param {string} [type]
|
|
41
|
+
* @returns {{ host: string, port: number, user: string, password: string, database: string } | null}
|
|
42
|
+
*/
|
|
43
|
+
function parseConnectionUrl(url, type) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(url)
|
|
46
|
+
const defaultPort = type === DB_TYPE_MYSQL ? 3306 : 5432
|
|
47
|
+
const defaultDb = type === DB_TYPE_MYSQL ? 'mysql' : 'postgres'
|
|
48
|
+
return {
|
|
49
|
+
host: parsed.hostname || 'localhost',
|
|
50
|
+
port: parseInt(parsed.port || String(defaultPort), 10),
|
|
51
|
+
user: parsed.username || '',
|
|
52
|
+
password: parsed.password || '',
|
|
53
|
+
database: (parsed.pathname || '/').replace(/^\//, '') || defaultDb,
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} env
|
|
62
|
+
* @param {string} url
|
|
63
|
+
* @param {number} connectionTimeoutMs
|
|
64
|
+
* @returns {{ pool: any, type: string }}
|
|
65
|
+
*/
|
|
66
|
+
function createDatabasePool(env, url, connectionTimeoutMs) {
|
|
67
|
+
const type = getDatabaseType(url)
|
|
68
|
+
|
|
69
|
+
if (type === DB_TYPE_MYSQL) {
|
|
70
|
+
const config = parseConnectionUrl(url, DB_TYPE_MYSQL)
|
|
71
|
+
if (!config) {
|
|
72
|
+
throw new Error(`Invalid MySQL URL for ${env}`)
|
|
73
|
+
}
|
|
74
|
+
const pool = mysql.createPool({
|
|
75
|
+
host: config.host,
|
|
76
|
+
port: config.port,
|
|
77
|
+
user: config.user,
|
|
78
|
+
password: config.password,
|
|
79
|
+
database: config.database,
|
|
80
|
+
connectionLimit: 1,
|
|
81
|
+
connectTimeout: connectionTimeoutMs,
|
|
82
|
+
waitForConnections: false,
|
|
83
|
+
})
|
|
84
|
+
return { pool, type: DB_TYPE_MYSQL }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pool = new Pool({
|
|
88
|
+
connectionString: url,
|
|
89
|
+
max: 1,
|
|
90
|
+
idleTimeoutMillis: 30000,
|
|
91
|
+
connectionTimeoutMillis: connectionTimeoutMs,
|
|
92
|
+
})
|
|
93
|
+
return { pool, type: DB_TYPE_POSTGRES }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {any} pool
|
|
98
|
+
* @param {string} type
|
|
99
|
+
* @returns {Promise<{ connectMs: number }>}
|
|
100
|
+
*/
|
|
101
|
+
async function runHealthCheck(pool, type) {
|
|
102
|
+
if (type === DB_TYPE_MYSQL) {
|
|
103
|
+
let connectMs = 0.0
|
|
104
|
+
|
|
105
|
+
/** @type {any} */
|
|
106
|
+
let conn
|
|
107
|
+
try {
|
|
108
|
+
const startConnect = nowNs()
|
|
109
|
+
conn = await pool.getConnection()
|
|
110
|
+
connectMs = nsToMs(nowNs() - startConnect)
|
|
111
|
+
|
|
112
|
+
await conn.query('SELECT 1')
|
|
113
|
+
return { connectMs }
|
|
114
|
+
} catch (err) {
|
|
115
|
+
err.healthCheckTimings = {
|
|
116
|
+
connectMs,
|
|
117
|
+
}
|
|
118
|
+
throw err
|
|
119
|
+
} finally {
|
|
120
|
+
if (conn) {
|
|
121
|
+
try {
|
|
122
|
+
// Destroy the connection so next check measures a real connect.
|
|
123
|
+
// mysql2 pooled connections support destroy() to remove it from the pool.
|
|
124
|
+
if (typeof conn.destroy === 'function') conn.destroy()
|
|
125
|
+
else if (typeof conn.end === 'function') await conn.end()
|
|
126
|
+
else if (typeof conn.release === 'function') conn.release()
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let connectMs = 0.0
|
|
135
|
+
|
|
136
|
+
/** @type {import('pg').PoolClient | null} */
|
|
137
|
+
let client = null
|
|
138
|
+
try {
|
|
139
|
+
const startConnect = nowNs()
|
|
140
|
+
client = await pool.connect()
|
|
141
|
+
connectMs = nsToMs(nowNs() - startConnect)
|
|
142
|
+
|
|
143
|
+
await client.query('SELECT 1')
|
|
144
|
+
return { connectMs }
|
|
145
|
+
} catch (err) {
|
|
146
|
+
err.healthCheckTimings = {
|
|
147
|
+
connectMs,
|
|
148
|
+
}
|
|
149
|
+
throw err
|
|
150
|
+
} finally {
|
|
151
|
+
if (client) {
|
|
152
|
+
try {
|
|
153
|
+
// Force the pool to drop the client so next check measures a real connect.
|
|
154
|
+
// In node-postgres, passing a truthy value removes the client from the pool.
|
|
155
|
+
client.release(true)
|
|
156
|
+
} catch {
|
|
157
|
+
// ignore
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {any} pool
|
|
165
|
+
* @returns {Promise<void>}
|
|
166
|
+
*/
|
|
167
|
+
async function closePool(pool) {
|
|
168
|
+
try {
|
|
169
|
+
await pool.end()
|
|
170
|
+
} catch {
|
|
171
|
+
// ignore
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
getDatabaseType,
|
|
177
|
+
parseConnectionUrl,
|
|
178
|
+
createDatabasePool,
|
|
179
|
+
runHealthCheck,
|
|
180
|
+
closePool,
|
|
181
|
+
DB_TYPE_POSTGRES,
|
|
182
|
+
DB_TYPE_MYSQL,
|
|
183
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const {
|
|
2
|
+
getRedisClientType,
|
|
3
|
+
REDIS_V4,
|
|
4
|
+
IOREDIS,
|
|
5
|
+
REDIS_V3,
|
|
6
|
+
} = require('../redisUtils')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HealthCheckCache provides a shared cache layer for health check results.
|
|
10
|
+
* It uses Redis if available for cross-process sharing, with graceful fallback
|
|
11
|
+
* to in-memory cache if Redis is not configured or unavailable.
|
|
12
|
+
*/
|
|
13
|
+
class HealthCheckCache {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {any} [options.redisClient]
|
|
17
|
+
* @param {string} [options.cacheKey] - Redis key (e.g. 'health:database:status')
|
|
18
|
+
* @param {string} [options.appName] - Used when cacheKey not set: healthcheck:${appName}
|
|
19
|
+
* @param {number} [options.staleThresholdMs]
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.redisClient = options.redisClient || null
|
|
23
|
+
this.appName =
|
|
24
|
+
options.appName || process.env.BUILD_APP_NAME || 'unknown-app'
|
|
25
|
+
this.staleThresholdMs = options.staleThresholdMs ?? 180_000
|
|
26
|
+
this.cacheKey = options.cacheKey || `healthcheck:${this.appName}`
|
|
27
|
+
|
|
28
|
+
/** In-memory fallback cache */
|
|
29
|
+
this._memoryCache = null
|
|
30
|
+
this._memoryCacheTimestamp = null
|
|
31
|
+
|
|
32
|
+
if (this.redisClient) {
|
|
33
|
+
this._redisClientType = getRedisClientType(this.redisClient)
|
|
34
|
+
this._redisAvailable = true
|
|
35
|
+
} else {
|
|
36
|
+
this._redisAvailable = false
|
|
37
|
+
console.warn(
|
|
38
|
+
`[HealthCheckCache] Redis not configured for ${this.appName}, using in-memory cache only (not shared across processes)`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if Redis is available and working.
|
|
45
|
+
* @returns {Promise<boolean>}
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
async _checkRedisAvailable() {
|
|
49
|
+
if (!this.redisClient || !this._redisAvailable) {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
let pong
|
|
55
|
+
if (this._redisClientType === REDIS_V3) {
|
|
56
|
+
pong = await new Promise((resolve, reject) => {
|
|
57
|
+
this.redisClient.ping((err, result) => {
|
|
58
|
+
if (err) reject(err)
|
|
59
|
+
else resolve(result)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
} else if (
|
|
63
|
+
this._redisClientType === REDIS_V4 ||
|
|
64
|
+
this._redisClientType === IOREDIS
|
|
65
|
+
) {
|
|
66
|
+
pong = await this.redisClient.ping()
|
|
67
|
+
} else {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
return pong === 'PONG'
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (this._redisAvailable) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`[HealthCheckCache] Redis became unavailable: ${err.message}`
|
|
75
|
+
)
|
|
76
|
+
this._redisAvailable = false
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets cached health check result from Redis (if available) or in-memory cache.
|
|
84
|
+
* Throws error if Redis is configured but read fails (so caller can return proper error format).
|
|
85
|
+
* @returns {Promise<Object | null>} Cached result or null
|
|
86
|
+
* @throws {Error} If Redis is configured but read fails
|
|
87
|
+
*/
|
|
88
|
+
async get() {
|
|
89
|
+
if (this.redisClient) {
|
|
90
|
+
try {
|
|
91
|
+
let cachedStr
|
|
92
|
+
if (this._redisClientType === REDIS_V3) {
|
|
93
|
+
cachedStr = await new Promise((resolve, reject) => {
|
|
94
|
+
this.redisClient.get(this.cacheKey, (err, result) => {
|
|
95
|
+
if (err) reject(err)
|
|
96
|
+
else resolve(result)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
} else if (
|
|
100
|
+
this._redisClientType === REDIS_V4 ||
|
|
101
|
+
this._redisClientType === IOREDIS
|
|
102
|
+
) {
|
|
103
|
+
cachedStr = await this.redisClient.get(this.cacheKey)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (cachedStr) {
|
|
107
|
+
try {
|
|
108
|
+
const cached = JSON.parse(cachedStr)
|
|
109
|
+
if (cached.result && cached.timestamp) {
|
|
110
|
+
this._memoryCache = cached.result
|
|
111
|
+
this._memoryCacheTimestamp = cached.timestamp
|
|
112
|
+
return cached.result
|
|
113
|
+
}
|
|
114
|
+
} catch (parseErr) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[HealthCheckCache] Failed to parse Redis cache:`,
|
|
117
|
+
parseErr.message
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
} catch (redisErr) {
|
|
123
|
+
this._redisAvailable = false
|
|
124
|
+
throw new Error(`Redis cache read failed: ${redisErr.message}`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (this._memoryCache && this._memoryCacheTimestamp) {
|
|
129
|
+
return this._memoryCache
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sets cached health check result in Redis (if available) and in-memory.
|
|
137
|
+
* @param {Object} result - Health check result to cache
|
|
138
|
+
* @returns {Promise<void>}
|
|
139
|
+
*/
|
|
140
|
+
async set(result) {
|
|
141
|
+
const cacheData = {
|
|
142
|
+
result,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this._memoryCache = result
|
|
147
|
+
this._memoryCacheTimestamp = cacheData.timestamp
|
|
148
|
+
|
|
149
|
+
if (await this._checkRedisAvailable()) {
|
|
150
|
+
try {
|
|
151
|
+
const cacheStr = JSON.stringify(cacheData)
|
|
152
|
+
const ttlSeconds = Math.ceil(this.staleThresholdMs / 1000) + 60
|
|
153
|
+
|
|
154
|
+
if (this._redisClientType === REDIS_V3) {
|
|
155
|
+
await new Promise((resolve, reject) => {
|
|
156
|
+
this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr, err => {
|
|
157
|
+
if (err) reject(err)
|
|
158
|
+
else resolve()
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
} else if (this._redisClientType === REDIS_V4) {
|
|
162
|
+
await this.redisClient.set(this.cacheKey, cacheStr, {
|
|
163
|
+
EX: ttlSeconds,
|
|
164
|
+
})
|
|
165
|
+
} else if (this._redisClientType === IOREDIS) {
|
|
166
|
+
await this.redisClient.setex(this.cacheKey, ttlSeconds, cacheStr)
|
|
167
|
+
}
|
|
168
|
+
} catch (redisErr) {
|
|
169
|
+
console.warn(
|
|
170
|
+
`[HealthCheckCache] Redis write failed (in-memory cache updated):`,
|
|
171
|
+
redisErr.message
|
|
172
|
+
)
|
|
173
|
+
this._redisAvailable = false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Clears the cache (both Redis and in-memory).
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
async clear() {
|
|
183
|
+
this._memoryCache = null
|
|
184
|
+
this._memoryCacheTimestamp = null
|
|
185
|
+
|
|
186
|
+
if (await this._checkRedisAvailable()) {
|
|
187
|
+
try {
|
|
188
|
+
if (this._redisClientType === REDIS_V3) {
|
|
189
|
+
await new Promise((resolve, reject) => {
|
|
190
|
+
this.redisClient.del(this.cacheKey, err => {
|
|
191
|
+
if (err) reject(err)
|
|
192
|
+
else resolve()
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
} else if (
|
|
196
|
+
this._redisClientType === REDIS_V4 ||
|
|
197
|
+
this._redisClientType === IOREDIS
|
|
198
|
+
) {
|
|
199
|
+
await this.redisClient.del(this.cacheKey)
|
|
200
|
+
}
|
|
201
|
+
} catch (redisErr) {
|
|
202
|
+
console.warn(`[HealthCheckCache] Redis clear failed:`, redisErr.message)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Checks if Redis is configured and available.
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
isRedisAvailable() {
|
|
212
|
+
return this._redisAvailable && this.redisClient !== null
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { HealthCheckCache }
|