@adalo/metrics 0.1.162 → 0.1.164
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/lib/metrics/baseMetricsClient.d.ts.map +1 -1
- package/lib/metrics/baseMetricsClient.js +1 -4
- package/lib/metrics/baseMetricsClient.js.map +1 -1
- package/lib/metrics/metricsRedisClient.d.ts +30 -10
- package/lib/metrics/metricsRedisClient.d.ts.map +1 -1
- package/lib/metrics/metricsRedisClient.js +172 -86
- package/lib/metrics/metricsRedisClient.js.map +1 -1
- package/package.json +1 -1
- package/src/metrics/baseMetricsClient.js +1 -8
- package/src/metrics/metricsRedisClient.js +184 -138
|
@@ -9,6 +9,9 @@ const {
|
|
|
9
9
|
const redisConnectionStableFields = ['name', 'flags', 'cmd']
|
|
10
10
|
const redisConnectionFields = ['name', 'flags', 'tot-mem', 'cmd']
|
|
11
11
|
|
|
12
|
+
/** Stream entries older than this (ms) are trimmed so messages are not kept in Redis. Fixed 60s. */
|
|
13
|
+
const GRACEFUL_SHUTDOWN_STREAM_MAXAGE_MS = 60000
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* RedisMetricsClient extends BaseMetricsClient to collect
|
|
14
17
|
* Redis metrics periodically and push them to Prometheus Pushgateway.
|
|
@@ -32,7 +35,11 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
32
35
|
* @param {function} [options.startupValidation] - Function to validate startup (from BaseMetricsClient)
|
|
33
36
|
* @param {boolean} [options.disablePushgateway] - Disable pushing to Pushgateway (use HTTP scraping instead)
|
|
34
37
|
*/
|
|
35
|
-
constructor({ redisClient,
|
|
38
|
+
constructor({ redisClient, ...metricsConfig } = {}) {
|
|
39
|
+
if (redisClient == null) {
|
|
40
|
+
throw new Error('RedisMetricsClient requires redisClient')
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
const intervalSec =
|
|
37
44
|
metricsConfig.intervalSec ||
|
|
38
45
|
parseInt(process.env.METRICS_QUEUE_INTERVAL_SEC || '', 10) ||
|
|
@@ -42,36 +49,16 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
42
49
|
...metricsConfig,
|
|
43
50
|
processType: metricsConfig.processType || 'queue-metrics',
|
|
44
51
|
intervalSec,
|
|
45
|
-
skipFirstPush: metricsConfig.skipFirstPush !== false,
|
|
46
52
|
})
|
|
47
53
|
|
|
48
54
|
/** Redis client used for metrics */
|
|
49
55
|
this.redisClient = redisClient
|
|
50
56
|
this.redisClientType = getRedisClientType(redisClient)
|
|
51
57
|
|
|
52
|
-
/** When true, new instance publishes once to Redis on start; old instances subscribed to same channel exit immediately. Channel = metrics:graceful-shutdown:{app}:{process_type}. Default true; set METRICS_GRACEFUL_SHUTDOWN_REDIS=false or gracefulShutdownRedis: false to disable. */
|
|
53
|
-
const disabledByParam = gracefulShutdownRedis === false
|
|
54
|
-
const disabledByEnv =
|
|
55
|
-
process.env.METRICS_GRACEFUL_SHUTDOWN_REDIS === 'false'
|
|
56
|
-
this._gracefulShutdownRedis = !disabledByParam && !disabledByEnv
|
|
57
|
-
this._gracefulShutdownChannel = this._gracefulShutdownRedis
|
|
58
|
-
? `metrics:graceful-shutdown:${this.appName}:${this.processType}`
|
|
59
|
-
: null
|
|
60
58
|
/** Dedicated Redis connection for subscribe (subscriber mode); closed in cleanup. */
|
|
61
59
|
this._subClient = null
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
this._gracefulShutdownLogPrefix = `[graceful-shutdown] [${this.processType}] [${this.appName}] [${this.dynoId}]`
|
|
65
|
-
|
|
66
|
-
console.info(
|
|
67
|
-
`${this._gracefulShutdownLogPrefix} startup: graceful_shutdown_redis=${
|
|
68
|
-
this._gracefulShutdownRedis
|
|
69
|
-
} channel=${this._gracefulShutdownChannel || 'none'}`
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
if (this._gracefulShutdownRedis && this._gracefulShutdownChannel) {
|
|
73
|
-
this._setupGracefulShutdownSubscribe()
|
|
74
|
-
}
|
|
61
|
+
this._initGracefulShutdown(metricsConfig.gracefulShutdownRedis)
|
|
75
62
|
|
|
76
63
|
/** Counter for Redis client connections */
|
|
77
64
|
this.redisConnectionsGauge = this.createGauge({
|
|
@@ -113,139 +100,207 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
113
100
|
}
|
|
114
101
|
|
|
115
102
|
/**
|
|
116
|
-
*
|
|
103
|
+
* Initialize graceful-shutdown state and subscribe when enabled. Called from constructor.
|
|
104
|
+
* @param {boolean} [gracefulShutdownRedis] - Explicit false to disable; otherwise enabled unless METRICS_GRACEFUL_SHUTDOWN_REDIS=false.
|
|
117
105
|
*/
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
106
|
+
_initGracefulShutdown(gracefulShutdownRedis) {
|
|
107
|
+
const disabledByParam = gracefulShutdownRedis === false
|
|
108
|
+
const disabledByEnv =
|
|
109
|
+
process.env.METRICS_GRACEFUL_SHUTDOWN_REDIS === 'false'
|
|
110
|
+
this._gracefulShutdownRedis = !disabledByParam && !disabledByEnv
|
|
111
|
+
this._gracefulShutdownStream = this._gracefulShutdownRedis
|
|
112
|
+
? `metrics:graceful-shutdown:${this.appName}:${this.processType}`
|
|
113
|
+
: null
|
|
114
|
+
this._gracefulShutdownAckChannel = this._gracefulShutdownRedis
|
|
115
|
+
? `metrics:graceful-shutdown-ack:${this.appName}:${this.processType}`
|
|
116
|
+
: null
|
|
117
|
+
this._gracefulShutdownLogPrefix = `[graceful-shutdown] [${this.processType}] [${this.appName}] [${this.dynoId}]`
|
|
118
|
+
if (this._gracefulShutdownRedis && this._gracefulShutdownStream) {
|
|
119
|
+
this._setupGracefulShutdownSubscribe()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a dedicated Redis client for subscribe (pub/sub). Uses duplicate() when available, else createClient from REDIS_URL. Branches by redisClientType (IOREDIS, REDIS_V3, REDIS_V4).
|
|
125
|
+
* @returns {any|null} Subscriber client or null if not possible.
|
|
126
|
+
*/
|
|
127
|
+
_createSubscriberClient() {
|
|
128
|
+
if (this.redisClientType === IOREDIS && this.redisClient && typeof this.redisClient.duplicate === 'function') {
|
|
129
|
+
return this.redisClient.duplicate()
|
|
130
|
+
}
|
|
131
|
+
if ((this.redisClientType === REDIS_V3 || this.redisClientType === REDIS_V4) && this.redisClient && typeof this.redisClient.duplicate === 'function') {
|
|
132
|
+
return this.redisClient.duplicate()
|
|
133
|
+
}
|
|
134
|
+
if ((this.redisClientType === REDIS_V3 || this.redisClientType === REDIS_V4) && process.env.REDIS_URL) {
|
|
129
135
|
try {
|
|
130
136
|
const redis = require('redis')
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
err.message
|
|
137
|
-
)
|
|
138
|
-
})
|
|
137
|
+
if (typeof redis.createClient !== 'function') return null
|
|
138
|
+
try {
|
|
139
|
+
return redis.createClient({ url: process.env.REDIS_URL })
|
|
140
|
+
} catch {
|
|
141
|
+
return redis.createClient(process.env.REDIS_URL)
|
|
139
142
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.info(
|
|
145
|
-
`${this._gracefulShutdownLogPrefix} setup_subscribe: could not create subscriber from REDIS_URL:`,
|
|
146
|
-
e.message
|
|
147
|
-
)
|
|
143
|
+
} catch {
|
|
144
|
+
return null
|
|
148
145
|
}
|
|
149
146
|
}
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Set up Redis subscribe for graceful shutdown. Uses _createSubscriberClient() so subscriber matches client type (ioredis vs node-redis v3/v4).
|
|
152
|
+
*/
|
|
153
|
+
/**
|
|
154
|
+
* Set up Redis stream read for graceful shutdown. Uses _createSubscriberClient() and XREAD BLOCK; stream is trimmed (MAXLEN and MINID) so messages are not kept in Redis forever.
|
|
155
|
+
*/
|
|
156
|
+
_setupGracefulShutdownSubscribe() {
|
|
157
|
+
const streamKey = this._gracefulShutdownStream
|
|
158
|
+
if (!streamKey) return
|
|
159
|
+
|
|
160
|
+
const subClient = this._createSubscriberClient()
|
|
161
|
+
if (subClient && typeof subClient.on === 'function') {
|
|
162
|
+
subClient.on('error', () => {})
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
|
|
159
|
-
`${this._gracefulShutdownLogPrefix} setup_subscribe: channel=${channel} hasSubClient=true`
|
|
160
|
-
)
|
|
165
|
+
if (!subClient) return
|
|
161
166
|
|
|
162
167
|
this._subClient = subClient
|
|
168
|
+
this._streamReadLoop(streamKey)
|
|
169
|
+
}
|
|
163
170
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
_streamReadLoop(streamKey) {
|
|
172
|
+
const self = this
|
|
173
|
+
const blockMs = 5000
|
|
174
|
+
|
|
175
|
+
const runRead = () => {
|
|
176
|
+
if (!self._subClient) return
|
|
177
|
+
self._xreadBlock(streamKey, blockMs)
|
|
178
|
+
.then((entries) => {
|
|
179
|
+
if (!entries || entries.length === 0) {
|
|
180
|
+
setImmediate(runRead)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
self._cleanupAndPublishAck()
|
|
184
|
+
})
|
|
185
|
+
.catch(() => {
|
|
186
|
+
setImmediate(runRead)
|
|
187
|
+
})
|
|
169
188
|
}
|
|
189
|
+
runRead()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_xreadBlock(streamKey, blockMs) {
|
|
193
|
+
const client = this._subClient || this.redisClient
|
|
194
|
+
if (!client) return Promise.resolve([])
|
|
170
195
|
|
|
171
196
|
if (this.redisClientType === REDIS_V3) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
subClient.on('message', (ch, _msg) => {
|
|
178
|
-
if (ch === channel) onMessage()
|
|
179
|
-
})
|
|
180
|
-
subClient.subscribe(channel)
|
|
181
|
-
} else if (this.redisClientType === REDIS_V4) {
|
|
182
|
-
subClient.on('subscribe', () => {
|
|
183
|
-
console.info(
|
|
184
|
-
`${this._gracefulShutdownLogPrefix} Subscribed to channel=${channel} (waiting for new-instance message).`
|
|
185
|
-
)
|
|
186
|
-
})
|
|
187
|
-
subClient.on('message', (ch, _msg) => {
|
|
188
|
-
if (ch === channel) onMessage()
|
|
189
|
-
})
|
|
190
|
-
subClient.subscribe(channel)
|
|
191
|
-
} else if (this.redisClientType === IOREDIS) {
|
|
192
|
-
subClient.subscribe(channel, (err, count) => {
|
|
193
|
-
if (err) {
|
|
194
|
-
console.error(
|
|
195
|
-
`${this._gracefulShutdownLogPrefix} Subscribe error:`,
|
|
196
|
-
err
|
|
197
|
-
)
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
console.info(
|
|
201
|
-
`${this._gracefulShutdownLogPrefix} Subscribed to channel=${channel} (waiting for new-instance message).`
|
|
202
|
-
)
|
|
203
|
-
})
|
|
204
|
-
subClient.on('message', (ch, _msg) => {
|
|
205
|
-
if (ch === channel) onMessage()
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
client.send_command('XREAD', ['BLOCK', String(blockMs), 'STREAMS', streamKey, '$'], (err, result) => {
|
|
199
|
+
if (err) return reject(err)
|
|
200
|
+
resolve(this._parseXreadReply(result, streamKey))
|
|
201
|
+
})
|
|
206
202
|
})
|
|
207
203
|
}
|
|
204
|
+
if (this.redisClientType === REDIS_V4) {
|
|
205
|
+
const p = client.sendCommand(['XREAD', 'BLOCK', String(blockMs), 'STREAMS', streamKey, '$'])
|
|
206
|
+
return Promise.resolve(p).then(result => this._parseXreadReply(result, streamKey)).catch(() => [])
|
|
207
|
+
}
|
|
208
|
+
if (this.redisClientType === IOREDIS) {
|
|
209
|
+
const p = client.call('XREAD', 'BLOCK', blockMs, 'STREAMS', streamKey, '$')
|
|
210
|
+
return Promise.resolve(p).then(result => this._parseXreadReply(result, streamKey)).catch(() => [])
|
|
211
|
+
}
|
|
212
|
+
return Promise.resolve([])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_parseXreadReply(reply, streamKey) {
|
|
216
|
+
if (!reply || !Array.isArray(reply)) return []
|
|
217
|
+
const streamReply = reply.find(r => r && r[0] === streamKey)
|
|
218
|
+
if (!streamReply || !Array.isArray(streamReply[1])) return []
|
|
219
|
+
return streamReply[1].map(entry => (Array.isArray(entry) ? [entry[0], entry[1] || []] : [entry, []]))
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
/**
|
|
211
|
-
*
|
|
223
|
+
* Old instance: stop push, clear metrics from VM, then publish ack so new can start; then exit.
|
|
212
224
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
225
|
+
async _cleanupAndPublishAck() {
|
|
226
|
+
this.stopPush()
|
|
227
|
+
if (this.enabled) {
|
|
228
|
+
await this.gatewayDelete()
|
|
229
|
+
}
|
|
230
|
+
await this._publishAck()
|
|
231
|
+
if (this._subClient) {
|
|
232
|
+
try {
|
|
233
|
+
if (this.redisClientType === REDIS_V3) {
|
|
234
|
+
await new Promise((resolve, reject) => {
|
|
235
|
+
if (typeof this._subClient.quit === 'function') {
|
|
236
|
+
this._subClient.quit(err => (err ? reject(err) : resolve()))
|
|
237
|
+
} else resolve()
|
|
238
|
+
})
|
|
239
|
+
} else if (this.redisClientType === REDIS_V4) {
|
|
240
|
+
if (this._subClient.quit) await this._subClient.quit()
|
|
241
|
+
} else if (this.redisClientType === IOREDIS) {
|
|
242
|
+
if (this._subClient.disconnect) await this._subClient.disconnect()
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// ignore
|
|
228
246
|
}
|
|
247
|
+
this._subClient = null
|
|
229
248
|
}
|
|
249
|
+
try {
|
|
250
|
+
if (this.redisClient) {
|
|
251
|
+
if (this.redisClientType === REDIS_V3 || this.redisClientType === REDIS_V4) {
|
|
252
|
+
await this.redisClient.quit()
|
|
253
|
+
} else if (this.redisClientType === IOREDIS) {
|
|
254
|
+
await this.redisClient.disconnect()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// ignore
|
|
259
|
+
}
|
|
260
|
+
process.exit(0)
|
|
261
|
+
}
|
|
230
262
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
263
|
+
_publishAck() {
|
|
264
|
+
const ackChannel = this._gracefulShutdownAckChannel
|
|
265
|
+
if (!ackChannel || !this.redisClient) return Promise.resolve()
|
|
266
|
+
console.warn(
|
|
267
|
+
`${this._gracefulShutdownLogPrefix} OLD: clearing metrics and exiting.`
|
|
235
268
|
)
|
|
269
|
+
const msg = this.dynoId || 'stopped'
|
|
270
|
+
if (this.redisClientType === REDIS_V3) {
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
this.redisClient.send_command('PUBLISH', [ackChannel, msg], () => resolve())
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
if (this.redisClientType === REDIS_V4) {
|
|
276
|
+
return this.redisClient.sendCommand(['PUBLISH', ackChannel, msg]).catch(() => {})
|
|
277
|
+
}
|
|
278
|
+
if (this.redisClientType === IOREDIS) {
|
|
279
|
+
return this.redisClient.publish(ackChannel, msg).catch(() => {})
|
|
280
|
+
}
|
|
281
|
+
return Promise.resolve()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Publish "new instance started" to stream so old instances exit. Stream is trimmed (MAXLEN ~ 10 and MINID older than 1 min) so messages are not kept in Redis forever.
|
|
286
|
+
*/
|
|
287
|
+
_publishNewInstanceStarted() {
|
|
288
|
+
const streamKey = this._gracefulShutdownStream
|
|
289
|
+
if (!streamKey || !this.redisClient) return
|
|
290
|
+
const value = this.dynoId || '1'
|
|
291
|
+
const noop = () => {}
|
|
292
|
+
const maxAgeMs = GRACEFUL_SHUTDOWN_STREAM_MAXAGE_MS
|
|
293
|
+
const minId = `${Date.now() - maxAgeMs}-0`
|
|
236
294
|
|
|
237
295
|
if (this.redisClientType === REDIS_V3) {
|
|
238
|
-
this.redisClient.send_command('
|
|
296
|
+
this.redisClient.send_command('XADD', [streamKey, 'MAXLEN', '~', '10', '*', 'dyno_id', value], noop)
|
|
297
|
+
this.redisClient.send_command('XTRIM', [streamKey, 'MINID', minId], noop)
|
|
239
298
|
} else if (this.redisClientType === REDIS_V4) {
|
|
240
|
-
this.redisClient
|
|
241
|
-
|
|
242
|
-
.then(() => done())
|
|
243
|
-
.catch(done)
|
|
299
|
+
this.redisClient.sendCommand(['XADD', streamKey, 'MAXLEN', '~', '10', '*', 'dyno_id', value]).catch(noop)
|
|
300
|
+
this.redisClient.sendCommand(['XTRIM', streamKey, 'MINID', minId]).catch(noop)
|
|
244
301
|
} else if (this.redisClientType === IOREDIS) {
|
|
245
|
-
this.redisClient
|
|
246
|
-
|
|
247
|
-
.then(() => done())
|
|
248
|
-
.catch(done)
|
|
302
|
+
this.redisClient.call('XADD', streamKey, 'MAXLEN', '~', 10, '*', 'dyno_id', value).catch(noop)
|
|
303
|
+
this.redisClient.call('XTRIM', streamKey, 'MINID', minId).catch(noop)
|
|
249
304
|
}
|
|
250
305
|
}
|
|
251
306
|
|
|
@@ -498,7 +553,7 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
498
553
|
)
|
|
499
554
|
}
|
|
500
555
|
} catch (error) {
|
|
501
|
-
console.
|
|
556
|
+
console.error(
|
|
502
557
|
`[queue-metrics] Failed to collect Redis metrics:`,
|
|
503
558
|
error.message
|
|
504
559
|
)
|
|
@@ -514,14 +569,6 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
514
569
|
await this.collectRedisMetrics()
|
|
515
570
|
await this.gatewayPush()
|
|
516
571
|
this.clearAllCounters()
|
|
517
|
-
|
|
518
|
-
if (this.metricsLogValues) {
|
|
519
|
-
const metricObjects = await this.registry.getMetricsAsJSON()
|
|
520
|
-
console.info(
|
|
521
|
-
`[queue-metrics] Collected metrics for Redis`,
|
|
522
|
-
JSON.stringify(metricObjects, null, 2)
|
|
523
|
-
)
|
|
524
|
-
}
|
|
525
572
|
} catch (error) {
|
|
526
573
|
console.error(
|
|
527
574
|
`[queue-metrics] Failed to collect Redis metrics: ${error.message}`
|
|
@@ -531,8 +578,7 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
531
578
|
}
|
|
532
579
|
|
|
533
580
|
/**
|
|
534
|
-
* Start periodic collection.
|
|
535
|
-
* @param {number} [intervalSec=this.intervalSec] - Interval in seconds
|
|
581
|
+
* Start periodic collection. When graceful shutdown is on: new publishes "new started" so old exits and clears; new does not push until after one interval (skipFirstPush), giving old time to stop.
|
|
536
582
|
*/
|
|
537
583
|
startPush = (intervalSec = this.intervalSec) => {
|
|
538
584
|
this._startPush(intervalSec, () => {
|
|
@@ -587,7 +633,7 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
587
633
|
} catch (err) {
|
|
588
634
|
console.error('[queue-metrics] Error closing Redis client:', err)
|
|
589
635
|
}
|
|
590
|
-
|
|
636
|
+
await super.cleanup()
|
|
591
637
|
}
|
|
592
638
|
|
|
593
639
|
_setCleanupHandlers = () => {
|