@adalo/metrics 0.0.0-staging.28 → 0.0.0-staging.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/__tests__/httpMetricsRedisCollector.test.js +1 -1
- package/__tests__/httpMetricsRedisRecorder.test.js +1 -3
- package/__tests__/httpMetricsRedisStore.test.js +31 -38
- package/docs/http-metrics-redis.md +18 -0
- package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisCollector.js +2 -2
- package/lib/metrics/httpMetricsRedisCollector.js.map +1 -1
- package/lib/metrics/httpMetricsRedisRecorder.d.ts +1 -5
- package/lib/metrics/httpMetricsRedisRecorder.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisRecorder.js +2 -16
- package/lib/metrics/httpMetricsRedisRecorder.js.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.d.ts +4 -23
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.js +24 -96
- package/lib/metrics/httpMetricsRedisStore.js.map +1 -1
- package/lib/metrics/metricsClient.d.ts +1 -5
- package/lib/metrics/metricsClient.d.ts.map +1 -1
- package/lib/metrics/metricsClient.js +4 -16
- package/lib/metrics/metricsClient.js.map +1 -1
- package/package.json +1 -1
- package/src/metrics/httpMetricsRedisCollector.js +0 -6
- package/src/metrics/httpMetricsRedisRecorder.js +3 -35
- package/src/metrics/httpMetricsRedisStore.js +15 -119
- package/src/metrics/metricsClient.js +1 -30
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ new MetricsClient({
|
|
|
31
31
|
|
|
32
32
|
### HTTP metrics: `MetricsClient` vs Redis
|
|
33
33
|
|
|
34
|
-
**`MetricsClient`** only registers **in-process** `app_requests_*` counters when `METRICS_HTTP_ENABLED` / `httpMetricsEnabled` is on. For **Redis-backed** HTTP aggregation (multi-web / cluster), use **`HttpMetricsRedisRecorder`** (writers) and **`HttpMetricsRedisCollector`** (drain + push) with an injected **`redisClient`** — same idea as `RedisMetricsClient`, not mixed into `MetricsClient`. See
|
|
34
|
+
**`MetricsClient`** only registers **in-process** `app_requests_*` counters when `METRICS_HTTP_ENABLED` / `httpMetricsEnabled` is on. For **Redis-backed** HTTP aggregation (multi-web / cluster), use **`HttpMetricsRedisRecorder`** (writers) and **`HttpMetricsRedisCollector`** (drain + push) with an injected **`redisClient`** — same idea as `RedisMetricsClient`, not mixed into `MetricsClient`. See **[`docs/http-metrics-redis.md`](docs/http-metrics-redis.md)** (quick Redis reference) and the repo guide **[`../docs/METRICS.md`](../docs/METRICS.md)** (full architecture for devs and AI assistants).
|
|
35
35
|
|
|
36
36
|
`DatabaseMetricsClient` / `QueueRedisMetricsClient` / `RedisMetricsClient` call **`process.exit(0)` with no logs** if `BUILD_DYNO_PROCESS_TYPE` (or `processType`) does not match `database-metrics` / `queue-metrics` / (`redis-metrics` or `queue-metrics`) respectively.
|
|
37
37
|
## Example Usage
|
|
@@ -35,7 +35,7 @@ describe('HttpMetricsRedisCollector', () => {
|
|
|
35
35
|
|
|
36
36
|
it('pushMetrics drains Redis then completes without network push', async () => {
|
|
37
37
|
const redis = createRedisV3Mock()
|
|
38
|
-
const field = buildFieldKey('GET', '/health', 200
|
|
38
|
+
const field = buildFieldKey('GET', '/health', 200)
|
|
39
39
|
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
40
40
|
cb(null, [
|
|
41
41
|
[field, '1'],
|
|
@@ -44,14 +44,12 @@ describe('HttpMetricsRedisRecorder', () => {
|
|
|
44
44
|
})
|
|
45
45
|
const countKey = rec._store.countKey
|
|
46
46
|
const durKey = rec._store.durKey
|
|
47
|
-
const field = buildFieldKey('GET', '/p', 404
|
|
47
|
+
const field = buildFieldKey('GET', '/p', 404)
|
|
48
48
|
|
|
49
49
|
rec.trackHttpRequest({
|
|
50
50
|
method: 'GET',
|
|
51
51
|
route: '/p',
|
|
52
52
|
status_code: 404,
|
|
53
|
-
appId: 'x',
|
|
54
|
-
databaseId: 'y',
|
|
55
53
|
duration: 5,
|
|
56
54
|
})
|
|
57
55
|
await flushMicrotasks()
|
|
@@ -87,9 +87,9 @@ function createRedisV3InMemoryMock() {
|
|
|
87
87
|
|
|
88
88
|
describe('HttpMetricsRedisStore', () => {
|
|
89
89
|
describe('buildFieldKey', () => {
|
|
90
|
-
it('joins
|
|
91
|
-
expect(buildFieldKey('GET', '/api', 200
|
|
92
|
-
['GET', '/api', '200'
|
|
90
|
+
it('joins method, route, status with FIELD_SEP', () => {
|
|
91
|
+
expect(buildFieldKey('GET', '/api', 200)).toBe(
|
|
92
|
+
['GET', '/api', '200'].join(FIELD_SEP)
|
|
93
93
|
)
|
|
94
94
|
})
|
|
95
95
|
})
|
|
@@ -123,9 +123,9 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
123
123
|
processType: 'web',
|
|
124
124
|
ttlSec: 90,
|
|
125
125
|
})
|
|
126
|
-
const field = buildFieldKey('GET', '/x', 200
|
|
126
|
+
const field = buildFieldKey('GET', '/x', 200)
|
|
127
127
|
|
|
128
|
-
store.record('GET', '/x', 200,
|
|
128
|
+
store.record('GET', '/x', 200, 12)
|
|
129
129
|
await flushMicrotasks()
|
|
130
130
|
|
|
131
131
|
expect(redis.multi).toHaveBeenCalled()
|
|
@@ -140,7 +140,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
140
140
|
describe('flushToCounters', () => {
|
|
141
141
|
it('applies aggregated count and summed duration for one route (many requests → one field)', async () => {
|
|
142
142
|
const redis = createRedisV3Mock()
|
|
143
|
-
const field = buildFieldKey('GET', '/api/items', 200
|
|
143
|
+
const field = buildFieldKey('GET', '/api/items', 200)
|
|
144
144
|
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
145
145
|
cb(null, [
|
|
146
146
|
[field, '100'],
|
|
@@ -164,9 +164,6 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
164
164
|
method: 'GET',
|
|
165
165
|
route: '/api/items',
|
|
166
166
|
status_code: '200',
|
|
167
|
-
appId: '',
|
|
168
|
-
databaseId: '',
|
|
169
|
-
pid: '111',
|
|
170
167
|
},
|
|
171
168
|
100
|
|
172
169
|
)
|
|
@@ -175,9 +172,6 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
175
172
|
method: 'GET',
|
|
176
173
|
route: '/api/items',
|
|
177
174
|
status_code: '200',
|
|
178
|
-
appId: '',
|
|
179
|
-
databaseId: '',
|
|
180
|
-
pid: '111',
|
|
181
175
|
},
|
|
182
176
|
4500
|
|
183
177
|
)
|
|
@@ -185,7 +179,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
185
179
|
|
|
186
180
|
it('drains hashes and applies count and duration', async () => {
|
|
187
181
|
const redis = createRedisV3Mock()
|
|
188
|
-
const field = buildFieldKey('POST', '/r', 201
|
|
182
|
+
const field = buildFieldKey('POST', '/r', 201)
|
|
189
183
|
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
190
184
|
cb(null, [
|
|
191
185
|
[field, '2'],
|
|
@@ -216,9 +210,6 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
216
210
|
method: 'POST',
|
|
217
211
|
route: '/r',
|
|
218
212
|
status_code: '201',
|
|
219
|
-
appId: 'a1',
|
|
220
|
-
databaseId: 'd1',
|
|
221
|
-
pid: '222',
|
|
222
213
|
},
|
|
223
214
|
2
|
|
224
215
|
)
|
|
@@ -227,17 +218,14 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
227
218
|
method: 'POST',
|
|
228
219
|
route: '/r',
|
|
229
220
|
status_code: '201',
|
|
230
|
-
appId: 'a1',
|
|
231
|
-
databaseId: 'd1',
|
|
232
|
-
pid: '222',
|
|
233
221
|
},
|
|
234
222
|
50
|
|
235
223
|
)
|
|
236
224
|
})
|
|
237
225
|
|
|
238
|
-
it('legacy
|
|
226
|
+
it('legacy 6-part hash fields still drain (labels from first three segments)', async () => {
|
|
239
227
|
const redis = createRedisV3Mock()
|
|
240
|
-
const legacyField = ['GET', '/old', '200', '', ''].join(FIELD_SEP)
|
|
228
|
+
const legacyField = ['GET', '/old', '200', 'a', 'd', '999'].join(FIELD_SEP)
|
|
241
229
|
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
242
230
|
cb(null, [
|
|
243
231
|
[legacyField, '3'],
|
|
@@ -254,11 +242,19 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
254
242
|
const ok = await store.flushToCounters(applyCount, applyDuration)
|
|
255
243
|
expect(ok).toBe(true)
|
|
256
244
|
expect(applyCount).toHaveBeenCalledWith(
|
|
257
|
-
|
|
245
|
+
{
|
|
246
|
+
method: 'GET',
|
|
247
|
+
route: '/old',
|
|
248
|
+
status_code: '200',
|
|
249
|
+
},
|
|
258
250
|
3
|
|
259
251
|
)
|
|
260
252
|
expect(applyDuration).toHaveBeenCalledWith(
|
|
261
|
-
|
|
253
|
+
{
|
|
254
|
+
method: 'GET',
|
|
255
|
+
route: '/old',
|
|
256
|
+
status_code: '200',
|
|
257
|
+
},
|
|
262
258
|
9
|
|
263
259
|
)
|
|
264
260
|
})
|
|
@@ -292,7 +288,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
292
288
|
for (let i = 0; i < routes.length; i++) {
|
|
293
289
|
const method = i % 2 === 0 ? 'GET' : 'POST'
|
|
294
290
|
const status = i % 5 === 0 ? 500 : 200
|
|
295
|
-
store.record(method, routes[i], status,
|
|
291
|
+
store.record(method, routes[i], status, 10 + i)
|
|
296
292
|
}
|
|
297
293
|
expect(redis._fieldCount(store.countKey)).toBe(routes.length)
|
|
298
294
|
|
|
@@ -324,7 +320,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
324
320
|
})
|
|
325
321
|
const n = 80
|
|
326
322
|
for (let i = 0; i < n; i++) {
|
|
327
|
-
store.record('GET', '/hot', 200,
|
|
323
|
+
store.record('GET', '/hot', 200, 5)
|
|
328
324
|
}
|
|
329
325
|
expect(redis._fieldCount(store.countKey)).toBe(1)
|
|
330
326
|
|
|
@@ -338,14 +334,11 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
338
334
|
method: 'GET',
|
|
339
335
|
route: '/hot',
|
|
340
336
|
status_code: '200',
|
|
341
|
-
appId: 'a1',
|
|
342
|
-
databaseId: 'd1',
|
|
343
|
-
pid: String(process.pid),
|
|
344
337
|
}),
|
|
345
338
|
n
|
|
346
339
|
)
|
|
347
340
|
expect(applyDuration).toHaveBeenCalledWith(
|
|
348
|
-
expect.objectContaining({ route: '/hot'
|
|
341
|
+
expect.objectContaining({ route: '/hot' }),
|
|
349
342
|
n * 5
|
|
350
343
|
)
|
|
351
344
|
})
|
|
@@ -356,9 +349,9 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
356
349
|
const dynoA = new HttpMetricsRedisStore(opts)
|
|
357
350
|
const dynoB = new HttpMetricsRedisStore(opts)
|
|
358
351
|
|
|
359
|
-
dynoA.record('GET', '/a', 200,
|
|
360
|
-
dynoB.record('GET', '/b', 304,
|
|
361
|
-
dynoA.record('POST', '/c', 201,
|
|
352
|
+
dynoA.record('GET', '/a', 200, 3)
|
|
353
|
+
dynoB.record('GET', '/b', 304, 7)
|
|
354
|
+
dynoA.record('POST', '/c', 201, 11)
|
|
362
355
|
|
|
363
356
|
expect(redis._fieldCount(dynoA.countKey)).toBe(3)
|
|
364
357
|
|
|
@@ -384,7 +377,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
384
377
|
Promise.all(
|
|
385
378
|
Array.from({ length: 15 }, (_, j) => {
|
|
386
379
|
const route = `/w${worker}/r${j}`
|
|
387
|
-
store.record('GET', route, 200,
|
|
380
|
+
store.record('GET', route, 200, 2)
|
|
388
381
|
return Promise.resolve()
|
|
389
382
|
})
|
|
390
383
|
)
|
|
@@ -416,12 +409,12 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
416
409
|
processType: 'api',
|
|
417
410
|
})
|
|
418
411
|
const samples = [
|
|
419
|
-
['GET', '/x', 200,
|
|
420
|
-
['GET', '/x', 404,
|
|
421
|
-
['DELETE', '/x|y', 204,
|
|
412
|
+
['GET', '/x', 200, 1],
|
|
413
|
+
['GET', '/x', 404, 2],
|
|
414
|
+
['DELETE', '/x|y', 204, 3],
|
|
422
415
|
]
|
|
423
|
-
for (const [m, r, s,
|
|
424
|
-
store.record(m, r, s,
|
|
416
|
+
for (const [m, r, s, dur] of samples) {
|
|
417
|
+
store.record(m, r, s, dur)
|
|
425
418
|
}
|
|
426
419
|
|
|
427
420
|
const applyCount = jest.fn()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# HTTP metrics via Redis (`HttpMetricsRedisRecorder` / `HttpMetricsRedisCollector`)
|
|
2
|
+
|
|
3
|
+
This file is a **quick entry point** for the Redis-backed HTTP aggregation path. **Full architecture, env vars, and diagrams** are in the repository guide:
|
|
4
|
+
|
|
5
|
+
**[docs/METRICS.md](../../docs/METRICS.md)**
|
|
6
|
+
|
|
7
|
+
## Cheat sheet
|
|
8
|
+
|
|
9
|
+
| Concern | Detail |
|
|
10
|
+
|--------|--------|
|
|
11
|
+
| **Writers** | `HttpMetricsRedisRecorder` — Express middleware records `method`, `route`, `status_code`, duration into Redis hashes. |
|
|
12
|
+
| **Reader** | `HttpMetricsRedisCollector` — separate process calls `pushMetrics()`, which **drains** Redis (atomic Lua) and increments `app_requests_*`, then pushes to VM-agent. |
|
|
13
|
+
| **Redis key shape** | `metrics:http:v2:<encode(appName)>:<encode(processType)>:count` and `:dur` — two hashes per writer group. |
|
|
14
|
+
| **Hash field** | Three logical parts: method, route, HTTP status (joined with an internal `FIELD_SEP`). |
|
|
15
|
+
| **Prometheus labels** | `method`, `route`, `status_code` (plus default `app` / `process_type` without `dyno_id` on these counters). |
|
|
16
|
+
| **Backend** | Web: `backend/monitoring.ts` + `METRICS_HTTP_ENABLED`. Drain: `backend/http-metrics-collector.ts`, Procfile `http-metrics:`. |
|
|
17
|
+
|
|
18
|
+
For in-process HTTP metrics **without** Redis, see **`MetricsClient`** + `METRICS_HTTP_ENABLED` in **[docs/METRICS.md](../../docs/METRICS.md#http-metrics-two-valid-designs)**.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisCollector.js"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH;IACE;;;;;;;;;;;;;;;;OAgBG;IACH;qBAfW,OAAO,OAAO,EAAE,WAAW;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisCollector.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisCollector.js"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH;IACE;;;;;;;;;;;;;;;;OAgBG;IACH;qBAfW,OAAO,OAAO,EAAE,WAAW;;;;;;;;;;;;;;mBA6DrC;IA5BC,8BAKE;CA4CL"}
|
|
@@ -59,13 +59,13 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
59
59
|
this.createCounter({
|
|
60
60
|
name: 'app_requests_total',
|
|
61
61
|
help: 'Total number of HTTP requests',
|
|
62
|
-
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', '
|
|
62
|
+
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', 'status_code']),
|
|
63
63
|
useLabelsWithoutDynoId: true
|
|
64
64
|
});
|
|
65
65
|
this.createCounter({
|
|
66
66
|
name: 'app_requests_total_duration',
|
|
67
67
|
help: 'Total duration of HTTP requests in milliseconds',
|
|
68
|
-
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', '
|
|
68
|
+
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', 'status_code']),
|
|
69
69
|
useLabelsWithoutDynoId: true
|
|
70
70
|
});
|
|
71
71
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","defaultLabelsWithoutDynoId","app","appName","process_type","_store","processType","ttlSec","createCounter","name","help","labelNames","withDefaultLabelsWithoutDynoId","useLabelsWithoutDynoId","pushMetrics","countersFunctions","app_requests_total","app_requests_total_duration","flushToCounters","labels","value","_pushMetrics","module","exports"],"sources":["../../src/metrics/httpMetricsRedisCollector.js"],"sourcesContent":["const { BaseMetricsClient } = require('./baseMetricsClient')\nconst { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\n/**\n * Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),\n * applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).\n * **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.\n * `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.\n * Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).\n *\n * @extends BaseMetricsClient\n */\nclass HttpMetricsRedisCollector extends BaseMetricsClient {\n /**\n * @param {Object} [config]\n * @param {import('redis').RedisClient} config.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} [config.appName] Application name (defaults per {@link BaseMetricsClient})\n * @param {string} [config.dynoId] Dyno/instance ID\n * @param {string} [config.processType] Label `process_type` on push (default from env / base)\n * @param {boolean} [config.enabled] Enable collection and push\n * @param {boolean} [config.logValues] Log metric JSON to console\n * @param {string} [config.pushgatewayUrl] VM-agent import URL\n * @param {string} [config.pushgatewaySecret] Basic auth secret (Base64)\n * @param {number} [config.intervalSec] Push interval (seconds)\n * @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported\n * @param {function} [config.startupValidation] Run before first push\n * @param {boolean} [config.disablePushgateway] Skip POST to VM-agent\n * @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.\n * @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)\n */\n constructor(config = {}) {\n const { redisClient } = config\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisCollector: redisClient is required')\n }\n\n super({\n ...config,\n blockNodeDefaultMetrics: true,\n })\n\n const keyProcessType = config.redisProcessTypeForKeys || 'web'\n\n this.defaultLabelsWithoutDynoId = {\n app: this.appName,\n process_type: keyProcessType,\n }\n\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName: this.appName,\n processType: keyProcessType,\n ttlSec: config.ttlSec,\n })\n\n this.createCounter({\n name: 'app_requests_total',\n help: 'Total number of HTTP requests',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n '
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","defaultLabelsWithoutDynoId","app","appName","process_type","_store","processType","ttlSec","createCounter","name","help","labelNames","withDefaultLabelsWithoutDynoId","useLabelsWithoutDynoId","pushMetrics","countersFunctions","app_requests_total","app_requests_total_duration","flushToCounters","labels","value","_pushMetrics","module","exports"],"sources":["../../src/metrics/httpMetricsRedisCollector.js"],"sourcesContent":["const { BaseMetricsClient } = require('./baseMetricsClient')\nconst { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\n/**\n * Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),\n * applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).\n * **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.\n * `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.\n * Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).\n *\n * @extends BaseMetricsClient\n */\nclass HttpMetricsRedisCollector extends BaseMetricsClient {\n /**\n * @param {Object} [config]\n * @param {import('redis').RedisClient} config.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} [config.appName] Application name (defaults per {@link BaseMetricsClient})\n * @param {string} [config.dynoId] Dyno/instance ID\n * @param {string} [config.processType] Label `process_type` on push (default from env / base)\n * @param {boolean} [config.enabled] Enable collection and push\n * @param {boolean} [config.logValues] Log metric JSON to console\n * @param {string} [config.pushgatewayUrl] VM-agent import URL\n * @param {string} [config.pushgatewaySecret] Basic auth secret (Base64)\n * @param {number} [config.intervalSec] Push interval (seconds)\n * @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported\n * @param {function} [config.startupValidation] Run before first push\n * @param {boolean} [config.disablePushgateway] Skip POST to VM-agent\n * @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.\n * @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)\n */\n constructor(config = {}) {\n const { redisClient } = config\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisCollector: redisClient is required')\n }\n\n super({\n ...config,\n blockNodeDefaultMetrics: true,\n })\n\n const keyProcessType = config.redisProcessTypeForKeys || 'web'\n\n this.defaultLabelsWithoutDynoId = {\n app: this.appName,\n process_type: keyProcessType,\n }\n\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName: this.appName,\n processType: keyProcessType,\n ttlSec: config.ttlSec,\n })\n\n this.createCounter({\n name: 'app_requests_total',\n help: 'Total number of HTTP requests',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n 'status_code',\n ]),\n useLabelsWithoutDynoId: true,\n })\n\n this.createCounter({\n name: 'app_requests_total_duration',\n help: 'Total duration of HTTP requests in milliseconds',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n 'status_code',\n ]),\n useLabelsWithoutDynoId: true,\n })\n }\n\n /**\n * Drains Redis into counters, then runs gauge updates and VM-agent push ({@link BaseMetricsClient#_pushMetrics}).\n * @returns {Promise<void>}\n */\n pushMetrics = async () => {\n if (\n this._store &&\n this.countersFunctions?.app_requests_total &&\n this.countersFunctions?.app_requests_total_duration\n ) {\n await this._store.flushToCounters(\n (labels, value) =>\n this.countersFunctions.app_requests_total(labels, value),\n (labels, value) =>\n this.countersFunctions.app_requests_total_duration(labels, value)\n )\n }\n return this._pushMetrics()\n }\n}\n\nmodule.exports = { HttpMetricsRedisCollector }\n"],"mappings":";;AAAA,MAAM;EAAEA;AAAkB,CAAC,GAAGC,OAAO,CAAC,qBAAqB,CAAC;AAC5D,MAAM;EAAEC;AAAsB,CAAC,GAAGD,OAAO,CAAC,yBAAyB,CAAC;;AAEpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAME,yBAAyB,SAASH,iBAAiB,CAAC;EACxD;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACEI,WAAWA,CAACC,MAAM,GAAG,CAAC,CAAC,EAAE;IACvB,MAAM;MAAEC;IAAY,CAAC,GAAGD,MAAM;IAC9B,IAAIC,WAAW,IAAI,IAAI,EAAE;MACvB,MAAM,IAAIC,KAAK,CAAC,oDAAoD,CAAC;IACvE;IAEA,KAAK,CAAC;MACJ,GAAGF,MAAM;MACTG,uBAAuB,EAAE;IAC3B,CAAC,CAAC;IAEF,MAAMC,cAAc,GAAGJ,MAAM,CAACK,uBAAuB,IAAI,KAAK;IAE9D,IAAI,CAACC,0BAA0B,GAAG;MAChCC,GAAG,EAAE,IAAI,CAACC,OAAO;MACjBC,YAAY,EAAEL;IAChB,CAAC;IAED,IAAI,CAACM,MAAM,GAAG,IAAIb,qBAAqB,CAAC;MACtCI,WAAW;MACXO,OAAO,EAAE,IAAI,CAACA,OAAO;MACrBG,WAAW,EAAEP,cAAc;MAC3BQ,MAAM,EAAEZ,MAAM,CAACY;IACjB,CAAC,CAAC;IAEF,IAAI,CAACC,aAAa,CAAC;MACjBC,IAAI,EAAE,oBAAoB;MAC1BC,IAAI,EAAE,+BAA+B;MACrCC,UAAU,EAAE,IAAI,CAACC,8BAA8B,CAAC,CAC9C,QAAQ,EACR,OAAO,EACP,aAAa,CACd,CAAC;MACFC,sBAAsB,EAAE;IAC1B,CAAC,CAAC;IAEF,IAAI,CAACL,aAAa,CAAC;MACjBC,IAAI,EAAE,6BAA6B;MACnCC,IAAI,EAAE,iDAAiD;MACvDC,UAAU,EAAE,IAAI,CAACC,8BAA8B,CAAC,CAC9C,QAAQ,EACR,OAAO,EACP,aAAa,CACd,CAAC;MACFC,sBAAsB,EAAE;IAC1B,CAAC,CAAC;EACJ;;EAEA;AACF;AACA;AACA;EACEC,WAAW,GAAG,MAAAA,CAAA,KAAY;IACxB,IACE,IAAI,CAACT,MAAM,IACX,IAAI,CAACU,iBAAiB,EAAEC,kBAAkB,IAC1C,IAAI,CAACD,iBAAiB,EAAEE,2BAA2B,EACnD;MACA,MAAM,IAAI,CAACZ,MAAM,CAACa,eAAe,CAC/B,CAACC,MAAM,EAAEC,KAAK,KACZ,IAAI,CAACL,iBAAiB,CAACC,kBAAkB,CAACG,MAAM,EAAEC,KAAK,CAAC,EAC1D,CAACD,MAAM,EAAEC,KAAK,KACZ,IAAI,CAACL,iBAAiB,CAACE,2BAA2B,CAACE,MAAM,EAAEC,KAAK,CACpE,CAAC;IACH;IACA,OAAO,IAAI,CAACC,YAAY,CAAC,CAAC;EAC5B,CAAC;AACH;AAEAC,MAAM,CAACC,OAAO,GAAG;EAAE9B;AAA0B,CAAC","ignoreList":[]}
|
|
@@ -26,16 +26,12 @@ export class HttpMetricsRedisRecorder {
|
|
|
26
26
|
* @param {string} params.method
|
|
27
27
|
* @param {string} params.route
|
|
28
28
|
* @param {number} params.status_code
|
|
29
|
-
* @param {string} [params.appId]
|
|
30
|
-
* @param {string} [params.databaseId]
|
|
31
29
|
* @param {number} params.duration
|
|
32
30
|
*/
|
|
33
|
-
trackHttpRequest({ method, route, status_code,
|
|
31
|
+
trackHttpRequest({ method, route, status_code, duration }: {
|
|
34
32
|
method: string;
|
|
35
33
|
route: string;
|
|
36
34
|
status_code: number;
|
|
37
|
-
appId?: string | undefined;
|
|
38
|
-
databaseId?: string | undefined;
|
|
39
35
|
duration: number;
|
|
40
36
|
}): void;
|
|
41
37
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisRecorder.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACb,OAAO;QACP,WAAW;QACX,MAAM;OAe9B;IARC,oBAA8B;IAC9B,gBAA8B;IAC9B,8BAKE;IAGJ;;;;;;OAMG;IACH;QAL0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACS,QAAQ,EAAvB,MAAM;aAIhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAsBvC;CACF"}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
HttpMetricsRedisStore
|
|
5
|
-
httpMetricsTraceLog
|
|
4
|
+
HttpMetricsRedisStore
|
|
6
5
|
} = require('./httpMetricsRedisStore');
|
|
7
|
-
function trunc(s, max = 120) {
|
|
8
|
-
const t = String(s);
|
|
9
|
-
return t.length > max ? `${t.slice(0, max - 3)}...` : t;
|
|
10
|
-
}
|
|
11
6
|
|
|
12
7
|
/**
|
|
13
8
|
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
@@ -48,20 +43,15 @@ class HttpMetricsRedisRecorder {
|
|
|
48
43
|
* @param {string} params.method
|
|
49
44
|
* @param {string} params.route
|
|
50
45
|
* @param {number} params.status_code
|
|
51
|
-
* @param {string} [params.appId]
|
|
52
|
-
* @param {string} [params.databaseId]
|
|
53
46
|
* @param {number} params.duration
|
|
54
47
|
*/
|
|
55
48
|
trackHttpRequest({
|
|
56
49
|
method,
|
|
57
50
|
route,
|
|
58
51
|
status_code,
|
|
59
|
-
appId = '',
|
|
60
|
-
databaseId = '',
|
|
61
52
|
duration
|
|
62
53
|
}) {
|
|
63
|
-
|
|
64
|
-
this._store.record(method, route, status_code, appId, databaseId, duration);
|
|
54
|
+
this._store.record(method, route, status_code, duration);
|
|
65
55
|
}
|
|
66
56
|
|
|
67
57
|
/**
|
|
@@ -80,14 +70,10 @@ class HttpMetricsRedisRecorder {
|
|
|
80
70
|
const start = Date.now();
|
|
81
71
|
res.on('finish', () => {
|
|
82
72
|
const route = req.route?.path || req.path || 'unknown';
|
|
83
|
-
const appId = req.params?.appId || req.body?.appId || req.query?.appId || '';
|
|
84
|
-
const databaseId = req.params?.databaseId || req.body?.databaseId || req.query?.databaseId || req.params?.datasourceId || req.body?.datasourceId || req.query?.datasourceId || '';
|
|
85
73
|
this.trackHttpRequest({
|
|
86
74
|
method: req.method,
|
|
87
75
|
route,
|
|
88
76
|
status_code: res.statusCode,
|
|
89
|
-
appId,
|
|
90
|
-
databaseId,
|
|
91
77
|
duration: Date.now() - start
|
|
92
78
|
});
|
|
93
79
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisRecorder.js","names":["HttpMetricsRedisStore","
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.js","names":["HttpMetricsRedisStore","require","HttpMetricsRedisRecorder","constructor","redisClient","appName","processType","ttlSec","Error","resolvedAppName","process","env","BUILD_APP_NAME","_store","trackHttpRequest","method","route","status_code","duration","record","trackHttpRequestMiddleware","req","res","next","start","Date","now","on","path","statusCode","module","exports"],"sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"sourcesContent":["const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\n/**\n * Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).\n * Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.\n *\n * @see HttpMetricsRedisStore\n */\nclass HttpMetricsRedisRecorder {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} [opts.appName] Optional override; default same as {@link BaseMetricsClient}: `BUILD_APP_NAME` or `unknown-app`.\n * @param {string} [opts.processType] Redis key segment (default **`web`**, same as {@link HttpMetricsRedisCollector}).\n * @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).\n */\n constructor({ redisClient, appName, processType = 'web', ttlSec } = {}) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisRecorder: redisClient is required')\n }\n const resolvedAppName = appName || process.env.BUILD_APP_NAME || 'unknown-app'\n this.processType = processType\n this.appName = resolvedAppName\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName: resolvedAppName,\n processType,\n ttlSec,\n })\n }\n\n /**\n * @param {Object} params\n * @param {string} params.method\n * @param {string} params.route\n * @param {number} params.status_code\n * @param {number} params.duration\n */\n trackHttpRequest({ method, route, status_code, duration }) {\n this._store.record(method, route, status_code, duration)\n }\n\n /**\n * Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.\n * Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.\n *\n * @param {import('http').IncomingMessage} req\n * @param {import('http').ServerResponse} res\n * @param {function} next\n */\n trackHttpRequestMiddleware = (req, res, next) => {\n if (req.method === 'OPTIONS') {\n next()\n return\n }\n\n const start = Date.now()\n res.on('finish', () => {\n const route = req.route?.path || req.path || 'unknown'\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { HttpMetricsRedisRecorder }\n"],"mappings":";;AAAA,MAAM;EAAEA;AAAsB,CAAC,GAAGC,OAAO,CAAC,yBAAyB,CAAC;;AAEpE;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,wBAAwB,CAAC;EAC7B;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAAC;IAAEC,WAAW;IAAEC,OAAO;IAAEC,WAAW,GAAG,KAAK;IAAEC;EAAO,CAAC,GAAG,CAAC,CAAC,EAAE;IACtE,IAAIH,WAAW,IAAI,IAAI,EAAE;MACvB,MAAM,IAAII,KAAK,CAAC,mDAAmD,CAAC;IACtE;IACA,MAAMC,eAAe,GAAGJ,OAAO,IAAIK,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;IAC9E,IAAI,CAACN,WAAW,GAAGA,WAAW;IAC9B,IAAI,CAACD,OAAO,GAAGI,eAAe;IAC9B,IAAI,CAACI,MAAM,GAAG,IAAIb,qBAAqB,CAAC;MACtCI,WAAW;MACXC,OAAO,EAAEI,eAAe;MACxBH,WAAW;MACXC;IACF,CAAC,CAAC;EACJ;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;EACEO,gBAAgBA,CAAC;IAAEC,MAAM;IAAEC,KAAK;IAAEC,WAAW;IAAEC;EAAS,CAAC,EAAE;IACzD,IAAI,CAACL,MAAM,CAACM,MAAM,CAACJ,MAAM,EAAEC,KAAK,EAAEC,WAAW,EAAEC,QAAQ,CAAC;EAC1D;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACEE,0BAA0B,GAAGA,CAACC,GAAG,EAAEC,GAAG,EAAEC,IAAI,KAAK;IAC/C,IAAIF,GAAG,CAACN,MAAM,KAAK,SAAS,EAAE;MAC5BQ,IAAI,CAAC,CAAC;MACN;IACF;IAEA,MAAMC,KAAK,GAAGC,IAAI,CAACC,GAAG,CAAC,CAAC;IACxBJ,GAAG,CAACK,EAAE,CAAC,QAAQ,EAAE,MAAM;MACrB,MAAMX,KAAK,GAAGK,GAAG,CAACL,KAAK,EAAEY,IAAI,IAAIP,GAAG,CAACO,IAAI,IAAI,SAAS;MAEtD,IAAI,CAACd,gBAAgB,CAAC;QACpBC,MAAM,EAAEM,GAAG,CAACN,MAAM;QAClBC,KAAK;QACLC,WAAW,EAAEK,GAAG,CAACO,UAAU;QAC3BX,QAAQ,EAAEO,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGF;MACzB,CAAC,CAAC;IACJ,CAAC,CAAC;IAEFD,IAAI,CAAC,CAAC;EACR,CAAC;AACH;AAEAO,MAAM,CAACC,OAAO,GAAG;EAAE7B;AAAyB,CAAC","ignoreList":[]}
|
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
3
3
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.
|
|
4
4
|
*
|
|
5
|
-
* **Structure:** `countKey` / `durKey` are each **one Redis hash**.
|
|
6
|
-
*
|
|
7
|
-
* appId, databaseId, pid)`. The **value** in `:count` is **HINCRBY 1** per request for that group; in `:dur`
|
|
8
|
-
* it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
|
|
9
|
-
* Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
|
|
5
|
+
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes
|
|
6
|
+
* `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.
|
|
10
7
|
*/
|
|
11
8
|
export class HttpMetricsRedisStore {
|
|
12
9
|
/**
|
|
@@ -35,11 +32,9 @@ export class HttpMetricsRedisStore {
|
|
|
35
32
|
* @param {string} method
|
|
36
33
|
* @param {string} route
|
|
37
34
|
* @param {number} statusCode
|
|
38
|
-
* @param {string} appId
|
|
39
|
-
* @param {string} databaseId
|
|
40
35
|
* @param {number} durationMs
|
|
41
36
|
*/
|
|
42
|
-
record(method: string, route: string, statusCode: number,
|
|
37
|
+
record(method: string, route: string, statusCode: number, durationMs: number): void;
|
|
43
38
|
/**
|
|
44
39
|
* @param {(labels: Object, value: number) => void} applyCount
|
|
45
40
|
* @param {(labels: Object, value: number) => void} applyDuration
|
|
@@ -51,31 +46,17 @@ export class HttpMetricsRedisStore {
|
|
|
51
46
|
* @param {string} method
|
|
52
47
|
* @param {string} route
|
|
53
48
|
* @param {number} statusCode
|
|
54
|
-
* @param {string} appId
|
|
55
|
-
* @param {string} databaseId
|
|
56
|
-
* @param {string} pid - OS process id (string); distinguishes cluster workers / processes in one Redis hash
|
|
57
49
|
* @returns {string}
|
|
58
50
|
*/
|
|
59
|
-
export function buildFieldKey(method: string, route: string, statusCode: number
|
|
51
|
+
export function buildFieldKey(method: string, route: string, statusCode: number): string;
|
|
60
52
|
/**
|
|
61
53
|
* Record separator for hash fields (avoids collisions when route contains "|").
|
|
62
54
|
* @type {string}
|
|
63
55
|
*/
|
|
64
56
|
export const FIELD_SEP: string;
|
|
65
|
-
/**
|
|
66
|
-
* @returns {boolean} Whether the `redis` npm package is resolvable (optional peer).
|
|
67
|
-
*/
|
|
68
|
-
export function isRedisPeerInstalled(): boolean;
|
|
69
57
|
/**
|
|
70
58
|
* Default Redis key TTL in seconds (sliding: refreshed on each `record` write).
|
|
71
59
|
* @type {number}
|
|
72
60
|
*/
|
|
73
61
|
export const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC: number;
|
|
74
|
-
/**
|
|
75
|
-
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
76
|
-
* Single `console.log` only: many hosts (e.g. Heroku) merge stdout+stderr into one stream and would
|
|
77
|
-
* show duplicate lines if we also wrote the same text to stderr.
|
|
78
|
-
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
79
|
-
*/
|
|
80
|
-
export function httpMetricsTraceLog(body: string): void;
|
|
81
62
|
//# sourceMappingURL=httpMetricsRedisStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"AA0CA;;;;;;GAMG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACd,OAAO,EAApB,MAAM;QACO,WAAW,EAAxB,MAAM;QACQ,MAAM;OAgB9B;IAVC,qCAA0B;IAC1B,eAGwC;IAIxC,iBAAiD;IACjD,eAA6C;IAG/C;;;OAGG;IACH,sBAEC;IAED;;;;;OAKG;IACH,eALW,MAAM,SACN,MAAM,cACN,MAAM,cACN,MAAM,QAqBhB;IAED;;;;OAIG;IACH,qCAJoB,MAAM,SAAS,MAAM,KAAK,IAAI,0BAC9B,MAAM,SAAS,MAAM,KAAK,IAAI,GACrC,QAAQ,OAAO,CAAC,CAoE5B;CACF;AA/JD;;;;;GAKG;AACH,sCALW,MAAM,SACN,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AA7BD;;;GAGG;AACH,wBAFU,MAAM,CAEQ;AAExB;;;GAGG;AACH,iDAFU,MAAM,CAE8B"}
|