@adalo/metrics 0.0.0-staging.27 → 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 +54 -28
- 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 -22
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.js +24 -82
- 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 -4
- package/src/metrics/httpMetricsRedisRecorder.js +3 -35
- package/src/metrics/httpMetricsRedisStore.js +15 -96
- 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,8 +164,6 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
164
164
|
method: 'GET',
|
|
165
165
|
route: '/api/items',
|
|
166
166
|
status_code: '200',
|
|
167
|
-
appId: '',
|
|
168
|
-
databaseId: '',
|
|
169
167
|
},
|
|
170
168
|
100
|
|
171
169
|
)
|
|
@@ -174,8 +172,6 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
174
172
|
method: 'GET',
|
|
175
173
|
route: '/api/items',
|
|
176
174
|
status_code: '200',
|
|
177
|
-
appId: '',
|
|
178
|
-
databaseId: '',
|
|
179
175
|
},
|
|
180
176
|
4500
|
|
181
177
|
)
|
|
@@ -183,7 +179,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
183
179
|
|
|
184
180
|
it('drains hashes and applies count and duration', async () => {
|
|
185
181
|
const redis = createRedisV3Mock()
|
|
186
|
-
const field = buildFieldKey('POST', '/r', 201
|
|
182
|
+
const field = buildFieldKey('POST', '/r', 201)
|
|
187
183
|
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
188
184
|
cb(null, [
|
|
189
185
|
[field, '2'],
|
|
@@ -214,8 +210,6 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
214
210
|
method: 'POST',
|
|
215
211
|
route: '/r',
|
|
216
212
|
status_code: '201',
|
|
217
|
-
appId: 'a1',
|
|
218
|
-
databaseId: 'd1',
|
|
219
213
|
},
|
|
220
214
|
2
|
|
221
215
|
)
|
|
@@ -224,13 +218,47 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
224
218
|
method: 'POST',
|
|
225
219
|
route: '/r',
|
|
226
220
|
status_code: '201',
|
|
227
|
-
appId: 'a1',
|
|
228
|
-
databaseId: 'd1',
|
|
229
221
|
},
|
|
230
222
|
50
|
|
231
223
|
)
|
|
232
224
|
})
|
|
233
225
|
|
|
226
|
+
it('legacy 6-part hash fields still drain (labels from first three segments)', async () => {
|
|
227
|
+
const redis = createRedisV3Mock()
|
|
228
|
+
const legacyField = ['GET', '/old', '200', 'a', 'd', '999'].join(FIELD_SEP)
|
|
229
|
+
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
230
|
+
cb(null, [
|
|
231
|
+
[legacyField, '3'],
|
|
232
|
+
[legacyField, '9'],
|
|
233
|
+
])
|
|
234
|
+
})
|
|
235
|
+
const store = new HttpMetricsRedisStore({
|
|
236
|
+
redisClient: redis,
|
|
237
|
+
appName: 'app',
|
|
238
|
+
processType: 'web',
|
|
239
|
+
})
|
|
240
|
+
const applyCount = jest.fn()
|
|
241
|
+
const applyDuration = jest.fn()
|
|
242
|
+
const ok = await store.flushToCounters(applyCount, applyDuration)
|
|
243
|
+
expect(ok).toBe(true)
|
|
244
|
+
expect(applyCount).toHaveBeenCalledWith(
|
|
245
|
+
{
|
|
246
|
+
method: 'GET',
|
|
247
|
+
route: '/old',
|
|
248
|
+
status_code: '200',
|
|
249
|
+
},
|
|
250
|
+
3
|
|
251
|
+
)
|
|
252
|
+
expect(applyDuration).toHaveBeenCalledWith(
|
|
253
|
+
{
|
|
254
|
+
method: 'GET',
|
|
255
|
+
route: '/old',
|
|
256
|
+
status_code: '200',
|
|
257
|
+
},
|
|
258
|
+
9
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
|
|
234
262
|
it('resolves true with no applies when eval returns short array', async () => {
|
|
235
263
|
const redis = createRedisV3Mock()
|
|
236
264
|
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
@@ -260,7 +288,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
260
288
|
for (let i = 0; i < routes.length; i++) {
|
|
261
289
|
const method = i % 2 === 0 ? 'GET' : 'POST'
|
|
262
290
|
const status = i % 5 === 0 ? 500 : 200
|
|
263
|
-
store.record(method, routes[i], status,
|
|
291
|
+
store.record(method, routes[i], status, 10 + i)
|
|
264
292
|
}
|
|
265
293
|
expect(redis._fieldCount(store.countKey)).toBe(routes.length)
|
|
266
294
|
|
|
@@ -292,7 +320,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
292
320
|
})
|
|
293
321
|
const n = 80
|
|
294
322
|
for (let i = 0; i < n; i++) {
|
|
295
|
-
store.record('GET', '/hot', 200,
|
|
323
|
+
store.record('GET', '/hot', 200, 5)
|
|
296
324
|
}
|
|
297
325
|
expect(redis._fieldCount(store.countKey)).toBe(1)
|
|
298
326
|
|
|
@@ -306,8 +334,6 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
306
334
|
method: 'GET',
|
|
307
335
|
route: '/hot',
|
|
308
336
|
status_code: '200',
|
|
309
|
-
appId: 'a1',
|
|
310
|
-
databaseId: 'd1',
|
|
311
337
|
}),
|
|
312
338
|
n
|
|
313
339
|
)
|
|
@@ -323,9 +349,9 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
323
349
|
const dynoA = new HttpMetricsRedisStore(opts)
|
|
324
350
|
const dynoB = new HttpMetricsRedisStore(opts)
|
|
325
351
|
|
|
326
|
-
dynoA.record('GET', '/a', 200,
|
|
327
|
-
dynoB.record('GET', '/b', 304,
|
|
328
|
-
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)
|
|
329
355
|
|
|
330
356
|
expect(redis._fieldCount(dynoA.countKey)).toBe(3)
|
|
331
357
|
|
|
@@ -351,7 +377,7 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
351
377
|
Promise.all(
|
|
352
378
|
Array.from({ length: 15 }, (_, j) => {
|
|
353
379
|
const route = `/w${worker}/r${j}`
|
|
354
|
-
store.record('GET', route, 200,
|
|
380
|
+
store.record('GET', route, 200, 2)
|
|
355
381
|
return Promise.resolve()
|
|
356
382
|
})
|
|
357
383
|
)
|
|
@@ -383,12 +409,12 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
383
409
|
processType: 'api',
|
|
384
410
|
})
|
|
385
411
|
const samples = [
|
|
386
|
-
['GET', '/x', 200,
|
|
387
|
-
['GET', '/x', 404,
|
|
388
|
-
['DELETE', '/x|y', 204,
|
|
412
|
+
['GET', '/x', 200, 1],
|
|
413
|
+
['GET', '/x', 404, 2],
|
|
414
|
+
['DELETE', '/x|y', 204, 3],
|
|
389
415
|
]
|
|
390
|
-
for (const [m, r, s,
|
|
391
|
-
store.record(m, r, s,
|
|
416
|
+
for (const [m, r, s, dur] of samples) {
|
|
417
|
+
store.record(m, r, s, dur)
|
|
392
418
|
}
|
|
393
419
|
|
|
394
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)`. 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,30 +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
49
|
* @returns {string}
|
|
57
50
|
*/
|
|
58
|
-
export function buildFieldKey(method: string, route: string, statusCode: number
|
|
51
|
+
export function buildFieldKey(method: string, route: string, statusCode: number): string;
|
|
59
52
|
/**
|
|
60
53
|
* Record separator for hash fields (avoids collisions when route contains "|").
|
|
61
54
|
* @type {string}
|
|
62
55
|
*/
|
|
63
56
|
export const FIELD_SEP: string;
|
|
64
|
-
/**
|
|
65
|
-
* @returns {boolean} Whether the `redis` npm package is resolvable (optional peer).
|
|
66
|
-
*/
|
|
67
|
-
export function isRedisPeerInstalled(): boolean;
|
|
68
57
|
/**
|
|
69
58
|
* Default Redis key TTL in seconds (sliding: refreshed on each `record` write).
|
|
70
59
|
* @type {number}
|
|
71
60
|
*/
|
|
72
61
|
export const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC: number;
|
|
73
|
-
/**
|
|
74
|
-
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
75
|
-
* Single `console.log` only: many hosts (e.g. Heroku) merge stdout+stderr into one stream and would
|
|
76
|
-
* show duplicate lines if we also wrote the same text to stderr.
|
|
77
|
-
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
78
|
-
*/
|
|
79
|
-
export function httpMetricsTraceLog(body: string): void;
|
|
80
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"}
|
|
@@ -11,39 +11,6 @@ const FIELD_SEP = '\x1e';
|
|
|
11
11
|
* @type {number}
|
|
12
12
|
*/
|
|
13
13
|
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120;
|
|
14
|
-
const LOG = '[http-metrics-redis]';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* When Node runs behind `cluster`, HTTP is usually served from workers — include worker id in traces.
|
|
18
|
-
* @returns {string}
|
|
19
|
-
*/
|
|
20
|
-
function clusterHint() {
|
|
21
|
-
try {
|
|
22
|
-
// eslint-disable-next-line global-require
|
|
23
|
-
const c = require('cluster');
|
|
24
|
-
if (c.isWorker && c.worker != null) {
|
|
25
|
-
return ` cluster_worker=${c.worker.id}`;
|
|
26
|
-
}
|
|
27
|
-
} catch (_) {
|
|
28
|
-
/* cluster not in use */
|
|
29
|
-
}
|
|
30
|
-
return '';
|
|
31
|
-
}
|
|
32
|
-
function truncField(s, max = 96) {
|
|
33
|
-
const t = String(s);
|
|
34
|
-
return t.length > max ? `${t.slice(0, max - 3)}...` : t;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
39
|
-
* Single `console.log` only: many hosts (e.g. Heroku) merge stdout+stderr into one stream and would
|
|
40
|
-
* show duplicate lines if we also wrote the same text to stderr.
|
|
41
|
-
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
42
|
-
*/
|
|
43
|
-
function httpMetricsTraceLog(body) {
|
|
44
|
-
const line = `${LOG} ${body}${clusterHint()}`;
|
|
45
|
-
console.log(line);
|
|
46
|
-
}
|
|
47
14
|
const DRAIN_LUA = `
|
|
48
15
|
local function drain(key)
|
|
49
16
|
local v = redis.call('HGETALL', key)
|
|
@@ -53,28 +20,14 @@ end
|
|
|
53
20
|
return {drain(KEYS[1]), drain(KEYS[2])}
|
|
54
21
|
`;
|
|
55
22
|
|
|
56
|
-
/**
|
|
57
|
-
* @returns {boolean} Whether the `redis` npm package is resolvable (optional peer).
|
|
58
|
-
*/
|
|
59
|
-
function isRedisPeerInstalled() {
|
|
60
|
-
try {
|
|
61
|
-
require.resolve('redis');
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
23
|
/**
|
|
69
24
|
* @param {string} method
|
|
70
25
|
* @param {string} route
|
|
71
26
|
* @param {number} statusCode
|
|
72
|
-
* @param {string} appId
|
|
73
|
-
* @param {string} databaseId
|
|
74
27
|
* @returns {string}
|
|
75
28
|
*/
|
|
76
|
-
function buildFieldKey(method, route, statusCode
|
|
77
|
-
return [method, route, String(statusCode)
|
|
29
|
+
function buildFieldKey(method, route, statusCode) {
|
|
30
|
+
return [method, route, String(statusCode)].join(FIELD_SEP);
|
|
78
31
|
}
|
|
79
32
|
function hgetallPairsToObject(pairs) {
|
|
80
33
|
const o = {};
|
|
@@ -91,11 +44,8 @@ function hgetallPairsToObject(pairs) {
|
|
|
91
44
|
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
92
45
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.
|
|
93
46
|
*
|
|
94
|
-
* **Structure:** `countKey` / `durKey` are each **one Redis hash**.
|
|
95
|
-
*
|
|
96
|
-
* appId, databaseId)`. The **value** in `:count` is **HINCRBY 1** per request for that group; in `:dur`
|
|
97
|
-
* it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
|
|
98
|
-
* Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
|
|
47
|
+
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes
|
|
48
|
+
* `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.
|
|
99
49
|
*/
|
|
100
50
|
class HttpMetricsRedisStore {
|
|
101
51
|
/**
|
|
@@ -133,22 +83,17 @@ class HttpMetricsRedisStore {
|
|
|
133
83
|
* @param {string} method
|
|
134
84
|
* @param {string} route
|
|
135
85
|
* @param {number} statusCode
|
|
136
|
-
* @param {string} appId
|
|
137
|
-
* @param {string} databaseId
|
|
138
86
|
* @param {number} durationMs
|
|
139
87
|
*/
|
|
140
|
-
record(method, route, statusCode,
|
|
88
|
+
record(method, route, statusCode, durationMs) {
|
|
141
89
|
try {
|
|
142
90
|
const client = this._ensureClient();
|
|
143
|
-
const field = buildFieldKey(method, route, statusCode
|
|
91
|
+
const field = buildFieldKey(method, route, statusCode);
|
|
144
92
|
const dur = Math.max(0, Math.round(Number(durationMs) || 0));
|
|
145
|
-
httpMetricsTraceLog(`save_to_redis pid=${process.pid} countKey=${this.countKey} ` + `durKey=${this.durKey} field=${truncField(field)} durationMs=${dur} (before MULTI/EXEC)`);
|
|
146
93
|
client.multi().hincrby(this.countKey, field, 1).hincrby(this.durKey, field, dur).expire(this.countKey, this.ttlSec).expire(this.durKey, this.ttlSec).exec(err => {
|
|
147
94
|
if (err) {
|
|
148
95
|
console.error('[HttpMetricsRedisStore] record failed:', err.message);
|
|
149
|
-
return;
|
|
150
96
|
}
|
|
151
|
-
httpMetricsTraceLog(`save_to_redis_ok pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} (after MULTI/EXEC)`);
|
|
152
97
|
});
|
|
153
98
|
} catch (e) {
|
|
154
99
|
console.error('[HttpMetricsRedisStore] record:', e.message);
|
|
@@ -169,7 +114,6 @@ class HttpMetricsRedisStore {
|
|
|
169
114
|
return Promise.resolve(false);
|
|
170
115
|
}
|
|
171
116
|
return new Promise(resolve => {
|
|
172
|
-
httpMetricsTraceLog(`get_from_redis_try pid=${process.pid} countKey=${this.countKey} (EVAL drain)`);
|
|
173
117
|
client.eval(DRAIN_LUA, 2, this.countKey, this.durKey, (evalErr, raw) => {
|
|
174
118
|
if (evalErr) {
|
|
175
119
|
console.error('[HttpMetricsRedisStore] drain failed:', evalErr.message);
|
|
@@ -178,15 +122,12 @@ class HttpMetricsRedisStore {
|
|
|
178
122
|
}
|
|
179
123
|
try {
|
|
180
124
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
181
|
-
httpMetricsTraceLog(`get_from_redis_empty pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (lua returned no pairs)`);
|
|
182
125
|
resolve(true);
|
|
183
126
|
return;
|
|
184
127
|
}
|
|
185
128
|
const counts = hgetallPairsToObject(raw[0]);
|
|
186
129
|
const durs = hgetallPairsToObject(raw[1]);
|
|
187
130
|
const fieldKeys = Object.keys(counts);
|
|
188
|
-
let sumRequests = 0;
|
|
189
|
-
const samples = [];
|
|
190
131
|
for (const field of fieldKeys) {
|
|
191
132
|
const count = parseInt(counts[field], 10);
|
|
192
133
|
if (!count || count < 1) {
|
|
@@ -194,27 +135,30 @@ class HttpMetricsRedisStore {
|
|
|
194
135
|
}
|
|
195
136
|
const dur = parseInt(durs[field] || '0', 10) || 0;
|
|
196
137
|
const parts = field.split(FIELD_SEP);
|
|
197
|
-
|
|
198
|
-
|
|
138
|
+
let labels = null;
|
|
139
|
+
if (parts.length === 3) {
|
|
140
|
+
const [m, route, statusStr] = parts;
|
|
141
|
+
labels = {
|
|
142
|
+
method: m,
|
|
143
|
+
route,
|
|
144
|
+
status_code: statusStr
|
|
145
|
+
};
|
|
146
|
+
} else if (parts.length === 5 || parts.length === 6) {
|
|
147
|
+
const [m, route, statusStr] = parts;
|
|
148
|
+
labels = {
|
|
149
|
+
method: m,
|
|
150
|
+
route,
|
|
151
|
+
status_code: statusStr
|
|
152
|
+
};
|
|
199
153
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (samples.length < 3) {
|
|
203
|
-
samples.push(`${m} ${route} ${statusStr} x${count}`);
|
|
154
|
+
if (!labels) {
|
|
155
|
+
continue;
|
|
204
156
|
}
|
|
205
|
-
const labels = {
|
|
206
|
-
method: m,
|
|
207
|
-
route,
|
|
208
|
-
status_code: statusStr,
|
|
209
|
-
appId: aid,
|
|
210
|
-
databaseId: did
|
|
211
|
-
};
|
|
212
157
|
applyCount(labels, count);
|
|
213
158
|
if (dur > 0) {
|
|
214
159
|
applyDuration(labels, dur);
|
|
215
160
|
}
|
|
216
161
|
}
|
|
217
|
-
httpMetricsTraceLog(`get_from_redis_ok pid=${process.pid} countKey=${this.countKey} hash_fields=${fieldKeys.length} sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`);
|
|
218
162
|
resolve(true);
|
|
219
163
|
} catch (e) {
|
|
220
164
|
console.error('[HttpMetricsRedisStore] flush apply failed:', e.message);
|
|
@@ -228,8 +172,6 @@ module.exports = {
|
|
|
228
172
|
HttpMetricsRedisStore,
|
|
229
173
|
buildFieldKey,
|
|
230
174
|
FIELD_SEP,
|
|
231
|
-
|
|
232
|
-
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,
|
|
233
|
-
httpMetricsTraceLog
|
|
175
|
+
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC
|
|
234
176
|
};
|
|
235
177
|
//# sourceMappingURL=httpMetricsRedisStore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","LOG","clusterHint","c","require","isWorker","worker","id","_","truncField","s","max","t","String","length","slice","httpMetricsTraceLog","body","line","console","log","DRAIN_LUA","isRedisPeerInstalled","resolve","buildFieldKey","method","route","statusCode","appId","databaseId","join","hgetallPairsToObject","pairs","o","i","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","_ensureClient","record","durationMs","client","field","dur","Math","round","Number","process","pid","multi","hincrby","expire","exec","err","error","message","e","flushToCounters","applyCount","applyDuration","Promise","eval","evalErr","raw","Array","isArray","counts","durs","fieldKeys","Object","keys","sumRequests","samples","count","parseInt","parts","split","m","statusStr","aid","did","push","labels","status_code","module","exports"],"sources":["../../src/metrics/httpMetricsRedisStore.js"],"sourcesContent":["/**\n * Record separator for hash fields (avoids collisions when route contains \"|\").\n * @type {string}\n */\nconst FIELD_SEP = '\\x1e'\n\n/**\n * Default Redis key TTL in seconds (sliding: refreshed on each `record` write).\n * @type {number}\n */\nconst DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120\n\nconst LOG = '[http-metrics-redis]'\n\n/**\n * When Node runs behind `cluster`, HTTP is usually served from workers — include worker id in traces.\n * @returns {string}\n */\nfunction clusterHint() {\n try {\n // eslint-disable-next-line global-require\n const c = require('cluster')\n if (c.isWorker && c.worker != null) {\n return ` cluster_worker=${c.worker.id}`\n }\n } catch (_) {\n /* cluster not in use */\n }\n return ''\n}\n\nfunction truncField(s, max = 96) {\n const t = String(s)\n return t.length > max ? `${t.slice(0, max - 3)}...` : t\n}\n\n/**\n * Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).\n * Single `console.log` only: many hosts (e.g. Heroku) merge stdout+stderr into one stream and would\n * show duplicate lines if we also wrote the same text to stderr.\n * @param {string} body - Line body after `[http-metrics-redis]` prefix.\n */\nfunction httpMetricsTraceLog(body) {\n const line = `${LOG} ${body}${clusterHint()}`\n console.log(line)\n}\n\nconst DRAIN_LUA = `\nlocal function drain(key)\n local v = redis.call('HGETALL', key)\n redis.call('DEL', key)\n return v\nend\nreturn {drain(KEYS[1]), drain(KEYS[2])}\n`\n\n/**\n * @returns {boolean} Whether the `redis` npm package is resolvable (optional peer).\n */\nfunction isRedisPeerInstalled() {\n try {\n require.resolve('redis')\n return true\n } catch {\n return false\n }\n}\n\n/**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {string} appId\n * @param {string} databaseId\n * @returns {string}\n */\nfunction buildFieldKey(method, route, statusCode, appId, databaseId) {\n return [method, route, String(statusCode), appId, databaseId].join(FIELD_SEP)\n}\n\nfunction hgetallPairsToObject(pairs) {\n const o = {}\n if (!pairs || !pairs.length) {\n return o\n }\n for (let i = 0; i < pairs.length; i += 2) {\n o[pairs[i]] = pairs[i + 1]\n }\n return o\n}\n\n/**\n * Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).\n * Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.\n *\n * **Structure:** `countKey` / `durKey` are each **one Redis hash**. The hash has **many fields** — not\n * one bucket for all traffic. Each **field name** encodes one label group `(method, route, status_code,\n * appId, databaseId)`. The **value** in `:count` is **HINCRBY 1** per request for that group; in `:dur`\n * it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).\n * Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.\n */\nclass HttpMetricsRedisStore {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient\n * @param {string} opts.appName BUILD_APP_NAME (key segment)\n * @param {string} opts.processType logical process for key (e.g. web)\n * @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisStore: redisClient is required')\n }\n this._client = redisClient\n this.ttlSec =\n typeof ttlSec === 'number' && ttlSec > 0\n ? ttlSec\n : DEFAULT_HTTP_METRICS_REDIS_TTL_SEC\n const keySeg = `${encodeURIComponent(appName)}:${encodeURIComponent(\n processType\n )}`\n this.countKey = `metrics:http:v2:${keySeg}:count`\n this.durKey = `metrics:http:v2:${keySeg}:dur`\n }\n\n /**\n * @returns {import('redis').RedisClient}\n * @private\n */\n _ensureClient() {\n return this._client\n }\n\n /**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {string} appId\n * @param {string} databaseId\n * @param {number} durationMs\n */\n record(method, route, statusCode, appId, databaseId, durationMs) {\n try {\n const client = this._ensureClient()\n const field = buildFieldKey(method, route, statusCode, appId, databaseId)\n const dur = Math.max(0, Math.round(Number(durationMs) || 0))\n httpMetricsTraceLog(\n `save_to_redis pid=${process.pid} countKey=${this.countKey} ` +\n `durKey=${this.durKey} field=${truncField(field)} durationMs=${dur} (before MULTI/EXEC)`\n )\n client\n .multi()\n .hincrby(this.countKey, field, 1)\n .hincrby(this.durKey, field, dur)\n .expire(this.countKey, this.ttlSec)\n .expire(this.durKey, this.ttlSec)\n .exec(err => {\n if (err) {\n console.error('[HttpMetricsRedisStore] record failed:', err.message)\n return\n }\n httpMetricsTraceLog(\n `save_to_redis_ok pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} (after MULTI/EXEC)`\n )\n })\n } catch (e) {\n console.error('[HttpMetricsRedisStore] record:', e.message)\n }\n }\n\n /**\n * @param {(labels: Object, value: number) => void} applyCount\n * @param {(labels: Object, value: number) => void} applyDuration\n * @returns {Promise<boolean>}\n */\n flushToCounters(applyCount, applyDuration) {\n let client\n try {\n client = this._ensureClient()\n } catch (e) {\n console.error('[HttpMetricsRedisStore] flush:', e.message)\n return Promise.resolve(false)\n }\n return new Promise(resolve => {\n httpMetricsTraceLog(\n `get_from_redis_try pid=${process.pid} countKey=${this.countKey} (EVAL drain)`\n )\n client.eval(\n DRAIN_LUA,\n 2,\n this.countKey,\n this.durKey,\n (evalErr, raw) => {\n if (evalErr) {\n console.error(\n '[HttpMetricsRedisStore] drain failed:',\n evalErr.message\n )\n resolve(false)\n return\n }\n\n try {\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n httpMetricsTraceLog(\n `get_from_redis_empty pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (lua returned no pairs)`\n )\n resolve(true)\n return\n }\n const counts = hgetallPairsToObject(raw[0])\n const durs = hgetallPairsToObject(raw[1])\n const fieldKeys = Object.keys(counts)\n let sumRequests = 0\n const samples = []\n for (const field of fieldKeys) {\n const count = parseInt(counts[field], 10)\n if (!count || count < 1) {\n continue\n }\n const dur = parseInt(durs[field] || '0', 10) || 0\n const parts = field.split(FIELD_SEP)\n if (parts.length !== 5) {\n continue\n }\n const [m, route, statusStr, aid, did] = parts\n sumRequests += count\n if (samples.length < 3) {\n samples.push(`${m} ${route} ${statusStr} x${count}`)\n }\n const labels = {\n method: m,\n route,\n status_code: statusStr,\n appId: aid,\n databaseId: did,\n }\n applyCount(labels, count)\n if (dur > 0) {\n applyDuration(labels, dur)\n }\n }\n httpMetricsTraceLog(\n `get_from_redis_ok pid=${process.pid} countKey=${this.countKey} hash_fields=${\n fieldKeys.length\n } sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`\n )\n resolve(true)\n } catch (e) {\n console.error(\n '[HttpMetricsRedisStore] flush apply failed:',\n e.message\n )\n resolve(false)\n }\n }\n )\n })\n }\n}\n\nmodule.exports = {\n HttpMetricsRedisStore,\n buildFieldKey,\n FIELD_SEP,\n isRedisPeerInstalled,\n DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,\n httpMetricsTraceLog,\n}\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA,MAAMA,SAAS,GAAG,MAAM;;AAExB;AACA;AACA;AACA;AACA,MAAMC,kCAAkC,GAAG,GAAG;AAE9C,MAAMC,GAAG,GAAG,sBAAsB;;AAElC;AACA;AACA;AACA;AACA,SAASC,WAAWA,CAAA,EAAG;EACrB,IAAI;IACF;IACA,MAAMC,CAAC,GAAGC,OAAO,CAAC,SAAS,CAAC;IAC5B,IAAID,CAAC,CAACE,QAAQ,IAAIF,CAAC,CAACG,MAAM,IAAI,IAAI,EAAE;MAClC,OAAO,mBAAmBH,CAAC,CAACG,MAAM,CAACC,EAAE,EAAE;IACzC;EACF,CAAC,CAAC,OAAOC,CAAC,EAAE;IACV;EAAA;EAEF,OAAO,EAAE;AACX;AAEA,SAASC,UAAUA,CAACC,CAAC,EAAEC,GAAG,GAAG,EAAE,EAAE;EAC/B,MAAMC,CAAC,GAAGC,MAAM,CAACH,CAAC,CAAC;EACnB,OAAOE,CAAC,CAACE,MAAM,GAAGH,GAAG,GAAG,GAAGC,CAAC,CAACG,KAAK,CAAC,CAAC,EAAEJ,GAAG,GAAG,CAAC,CAAC,KAAK,GAAGC,CAAC;AACzD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,SAASI,mBAAmBA,CAACC,IAAI,EAAE;EACjC,MAAMC,IAAI,GAAG,GAAGjB,GAAG,IAAIgB,IAAI,GAAGf,WAAW,CAAC,CAAC,EAAE;EAC7CiB,OAAO,CAACC,GAAG,CAACF,IAAI,CAAC;AACnB;AAEA,MAAMG,SAAS,GAAG;AAClB;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED;AACA;AACA;AACA,SAASC,oBAAoBA,CAAA,EAAG;EAC9B,IAAI;IACFlB,OAAO,CAACmB,OAAO,CAAC,OAAO,CAAC;IACxB,OAAO,IAAI;EACb,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,aAAaA,CAACC,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,EAAE;EACnE,OAAO,CAACJ,MAAM,EAAEC,KAAK,EAAEb,MAAM,CAACc,UAAU,CAAC,EAAEC,KAAK,EAAEC,UAAU,CAAC,CAACC,IAAI,CAAC/B,SAAS,CAAC;AAC/E;AAEA,SAASgC,oBAAoBA,CAACC,KAAK,EAAE;EACnC,MAAMC,CAAC,GAAG,CAAC,CAAC;EACZ,IAAI,CAACD,KAAK,IAAI,CAACA,KAAK,CAAClB,MAAM,EAAE;IAC3B,OAAOmB,CAAC;EACV;EACA,KAAK,IAAIC,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGF,KAAK,CAAClB,MAAM,EAAEoB,CAAC,IAAI,CAAC,EAAE;IACxCD,CAAC,CAACD,KAAK,CAACE,CAAC,CAAC,CAAC,GAAGF,KAAK,CAACE,CAAC,GAAG,CAAC,CAAC;EAC5B;EACA,OAAOD,CAAC;AACV;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAME,qBAAqB,CAAC;EAC1B;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAAC;IAAEC,WAAW;IAAEC,OAAO;IAAEC,WAAW;IAAEC;EAAO,CAAC,EAAE;IACzD,IAAIH,WAAW,IAAI,IAAI,EAAE;MACvB,MAAM,IAAII,KAAK,CAAC,gDAAgD,CAAC;IACnE;IACA,IAAI,CAACC,OAAO,GAAGL,WAAW;IAC1B,IAAI,CAACG,MAAM,GACT,OAAOA,MAAM,KAAK,QAAQ,IAAIA,MAAM,GAAG,CAAC,GACpCA,MAAM,GACNxC,kCAAkC;IACxC,MAAM2C,MAAM,GAAG,GAAGC,kBAAkB,CAACN,OAAO,CAAC,IAAIM,kBAAkB,CACjEL,WACF,CAAC,EAAE;IACH,IAAI,CAACM,QAAQ,GAAG,mBAAmBF,MAAM,QAAQ;IACjD,IAAI,CAACG,MAAM,GAAG,mBAAmBH,MAAM,MAAM;EAC/C;;EAEA;AACF;AACA;AACA;EACEI,aAAaA,CAAA,EAAG;IACd,OAAO,IAAI,CAACL,OAAO;EACrB;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACEM,MAAMA,CAACvB,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,EAAEoB,UAAU,EAAE;IAC/D,IAAI;MACF,MAAMC,MAAM,GAAG,IAAI,CAACH,aAAa,CAAC,CAAC;MACnC,MAAMI,KAAK,GAAG3B,aAAa,CAACC,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,CAAC;MACzE,MAAMuB,GAAG,GAAGC,IAAI,CAAC1C,GAAG,CAAC,CAAC,EAAE0C,IAAI,CAACC,KAAK,CAACC,MAAM,CAACN,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;MAC5DjC,mBAAmB,CACjB,qBAAqBwC,OAAO,CAACC,GAAG,aAAa,IAAI,CAACZ,QAAQ,GAAG,GAC3D,UAAU,IAAI,CAACC,MAAM,UAAUrC,UAAU,CAAC0C,KAAK,CAAC,eAAeC,GAAG,sBACtE,CAAC;MACDF,MAAM,CACHQ,KAAK,CAAC,CAAC,CACPC,OAAO,CAAC,IAAI,CAACd,QAAQ,EAAEM,KAAK,EAAE,CAAC,CAAC,CAChCQ,OAAO,CAAC,IAAI,CAACb,MAAM,EAAEK,KAAK,EAAEC,GAAG,CAAC,CAChCQ,MAAM,CAAC,IAAI,CAACf,QAAQ,EAAE,IAAI,CAACL,MAAM,CAAC,CAClCoB,MAAM,CAAC,IAAI,CAACd,MAAM,EAAE,IAAI,CAACN,MAAM,CAAC,CAChCqB,IAAI,CAACC,GAAG,IAAI;QACX,IAAIA,GAAG,EAAE;UACP3C,OAAO,CAAC4C,KAAK,CAAC,wCAAwC,EAAED,GAAG,CAACE,OAAO,CAAC;UACpE;QACF;QACAhD,mBAAmB,CACjB,wBAAwBwC,OAAO,CAACC,GAAG,aAAa,IAAI,CAACZ,QAAQ,WAAW,IAAI,CAACC,MAAM,qBACrF,CAAC;MACH,CAAC,CAAC;IACN,CAAC,CAAC,OAAOmB,CAAC,EAAE;MACV9C,OAAO,CAAC4C,KAAK,CAAC,iCAAiC,EAAEE,CAAC,CAACD,OAAO,CAAC;IAC7D;EACF;;EAEA;AACF;AACA;AACA;AACA;EACEE,eAAeA,CAACC,UAAU,EAAEC,aAAa,EAAE;IACzC,IAAIlB,MAAM;IACV,IAAI;MACFA,MAAM,GAAG,IAAI,CAACH,aAAa,CAAC,CAAC;IAC/B,CAAC,CAAC,OAAOkB,CAAC,EAAE;MACV9C,OAAO,CAAC4C,KAAK,CAAC,gCAAgC,EAAEE,CAAC,CAACD,OAAO,CAAC;MAC1D,OAAOK,OAAO,CAAC9C,OAAO,CAAC,KAAK,CAAC;IAC/B;IACA,OAAO,IAAI8C,OAAO,CAAC9C,OAAO,IAAI;MAC5BP,mBAAmB,CACjB,0BAA0BwC,OAAO,CAACC,GAAG,aAAa,IAAI,CAACZ,QAAQ,eACjE,CAAC;MACDK,MAAM,CAACoB,IAAI,CACTjD,SAAS,EACT,CAAC,EACD,IAAI,CAACwB,QAAQ,EACb,IAAI,CAACC,MAAM,EACX,CAACyB,OAAO,EAAEC,GAAG,KAAK;QAChB,IAAID,OAAO,EAAE;UACXpD,OAAO,CAAC4C,KAAK,CACX,uCAAuC,EACvCQ,OAAO,CAACP,OACV,CAAC;UACDzC,OAAO,CAAC,KAAK,CAAC;UACd;QACF;QAEA,IAAI;UACF,IAAI,CAACiD,GAAG,IAAI,CAACC,KAAK,CAACC,OAAO,CAACF,GAAG,CAAC,IAAIA,GAAG,CAAC1D,MAAM,GAAG,CAAC,EAAE;YACjDE,mBAAmB,CACjB,4BAA4BwC,OAAO,CAACC,GAAG,aAAa,IAAI,CAACZ,QAAQ,uDACnE,CAAC;YACDtB,OAAO,CAAC,IAAI,CAAC;YACb;UACF;UACA,MAAMoD,MAAM,GAAG5C,oBAAoB,CAACyC,GAAG,CAAC,CAAC,CAAC,CAAC;UAC3C,MAAMI,IAAI,GAAG7C,oBAAoB,CAACyC,GAAG,CAAC,CAAC,CAAC,CAAC;UACzC,MAAMK,SAAS,GAAGC,MAAM,CAACC,IAAI,CAACJ,MAAM,CAAC;UACrC,IAAIK,WAAW,GAAG,CAAC;UACnB,MAAMC,OAAO,GAAG,EAAE;UAClB,KAAK,MAAM9B,KAAK,IAAI0B,SAAS,EAAE;YAC7B,MAAMK,KAAK,GAAGC,QAAQ,CAACR,MAAM,CAACxB,KAAK,CAAC,EAAE,EAAE,CAAC;YACzC,IAAI,CAAC+B,KAAK,IAAIA,KAAK,GAAG,CAAC,EAAE;cACvB;YACF;YACA,MAAM9B,GAAG,GAAG+B,QAAQ,CAACP,IAAI,CAACzB,KAAK,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC;YACjD,MAAMiC,KAAK,GAAGjC,KAAK,CAACkC,KAAK,CAACtF,SAAS,CAAC;YACpC,IAAIqF,KAAK,CAACtE,MAAM,KAAK,CAAC,EAAE;cACtB;YACF;YACA,MAAM,CAACwE,CAAC,EAAE5D,KAAK,EAAE6D,SAAS,EAAEC,GAAG,EAAEC,GAAG,CAAC,GAAGL,KAAK;YAC7CJ,WAAW,IAAIE,KAAK;YACpB,IAAID,OAAO,CAACnE,MAAM,GAAG,CAAC,EAAE;cACtBmE,OAAO,CAACS,IAAI,CAAC,GAAGJ,CAAC,IAAI5D,KAAK,IAAI6D,SAAS,KAAKL,KAAK,EAAE,CAAC;YACtD;YACA,MAAMS,MAAM,GAAG;cACblE,MAAM,EAAE6D,CAAC;cACT5D,KAAK;cACLkE,WAAW,EAAEL,SAAS;cACtB3D,KAAK,EAAE4D,GAAG;cACV3D,UAAU,EAAE4D;YACd,CAAC;YACDtB,UAAU,CAACwB,MAAM,EAAET,KAAK,CAAC;YACzB,IAAI9B,GAAG,GAAG,CAAC,EAAE;cACXgB,aAAa,CAACuB,MAAM,EAAEvC,GAAG,CAAC;YAC5B;UACF;UACApC,mBAAmB,CACjB,yBAAyBwC,OAAO,CAACC,GAAG,aAAa,IAAI,CAACZ,QAAQ,gBAC5DgC,SAAS,CAAC/D,MAAM,iBACDkE,WAAW,WAAWC,OAAO,CAACnD,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EACnE,CAAC;UACDP,OAAO,CAAC,IAAI,CAAC;QACf,CAAC,CAAC,OAAO0C,CAAC,EAAE;UACV9C,OAAO,CAAC4C,KAAK,CACX,6CAA6C,EAC7CE,CAAC,CAACD,OACJ,CAAC;UACDzC,OAAO,CAAC,KAAK,CAAC;QAChB;MACF,CACF,CAAC;IACH,CAAC,CAAC;EACJ;AACF;AAEAsE,MAAM,CAACC,OAAO,GAAG;EACf3D,qBAAqB;EACrBX,aAAa;EACbzB,SAAS;EACTuB,oBAAoB;EACpBtB,kCAAkC;EAClCgB;AACF,CAAC","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","DRAIN_LUA","buildFieldKey","method","route","statusCode","String","join","hgetallPairsToObject","pairs","o","length","i","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","_ensureClient","record","durationMs","client","field","dur","Math","max","round","Number","multi","hincrby","expire","exec","err","console","error","message","e","flushToCounters","applyCount","applyDuration","Promise","resolve","eval","evalErr","raw","Array","isArray","counts","durs","fieldKeys","Object","keys","count","parseInt","parts","split","labels","m","statusStr","status_code","module","exports"],"sources":["../../src/metrics/httpMetricsRedisStore.js"],"sourcesContent":["/**\n * Record separator for hash fields (avoids collisions when route contains \"|\").\n * @type {string}\n */\nconst FIELD_SEP = '\\x1e'\n\n/**\n * Default Redis key TTL in seconds (sliding: refreshed on each `record` write).\n * @type {number}\n */\nconst DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120\n\nconst DRAIN_LUA = `\nlocal function drain(key)\n local v = redis.call('HGETALL', key)\n redis.call('DEL', key)\n return v\nend\nreturn {drain(KEYS[1]), drain(KEYS[2])}\n`\n\n/**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @returns {string}\n */\nfunction buildFieldKey(method, route, statusCode) {\n return [method, route, String(statusCode)].join(FIELD_SEP)\n}\n\nfunction hgetallPairsToObject(pairs) {\n const o = {}\n if (!pairs || !pairs.length) {\n return o\n }\n for (let i = 0; i < pairs.length; i += 2) {\n o[pairs[i]] = pairs[i + 1]\n }\n return o\n}\n\n/**\n * Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).\n * Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.\n *\n * **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes\n * `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.\n */\nclass HttpMetricsRedisStore {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient\n * @param {string} opts.appName BUILD_APP_NAME (key segment)\n * @param {string} opts.processType logical process for key (e.g. web)\n * @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisStore: redisClient is required')\n }\n this._client = redisClient\n this.ttlSec =\n typeof ttlSec === 'number' && ttlSec > 0\n ? ttlSec\n : DEFAULT_HTTP_METRICS_REDIS_TTL_SEC\n const keySeg = `${encodeURIComponent(appName)}:${encodeURIComponent(\n processType\n )}`\n this.countKey = `metrics:http:v2:${keySeg}:count`\n this.durKey = `metrics:http:v2:${keySeg}:dur`\n }\n\n /**\n * @returns {import('redis').RedisClient}\n * @private\n */\n _ensureClient() {\n return this._client\n }\n\n /**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {number} durationMs\n */\n record(method, route, statusCode, durationMs) {\n try {\n const client = this._ensureClient()\n const field = buildFieldKey(method, route, statusCode)\n const dur = Math.max(0, Math.round(Number(durationMs) || 0))\n client\n .multi()\n .hincrby(this.countKey, field, 1)\n .hincrby(this.durKey, field, dur)\n .expire(this.countKey, this.ttlSec)\n .expire(this.durKey, this.ttlSec)\n .exec(err => {\n if (err) {\n console.error('[HttpMetricsRedisStore] record failed:', err.message)\n }\n })\n } catch (e) {\n console.error('[HttpMetricsRedisStore] record:', e.message)\n }\n }\n\n /**\n * @param {(labels: Object, value: number) => void} applyCount\n * @param {(labels: Object, value: number) => void} applyDuration\n * @returns {Promise<boolean>}\n */\n flushToCounters(applyCount, applyDuration) {\n let client\n try {\n client = this._ensureClient()\n } catch (e) {\n console.error('[HttpMetricsRedisStore] flush:', e.message)\n return Promise.resolve(false)\n }\n return new Promise(resolve => {\n client.eval(\n DRAIN_LUA,\n 2,\n this.countKey,\n this.durKey,\n (evalErr, raw) => {\n if (evalErr) {\n console.error(\n '[HttpMetricsRedisStore] drain failed:',\n evalErr.message\n )\n resolve(false)\n return\n }\n\n try {\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n resolve(true)\n return\n }\n const counts = hgetallPairsToObject(raw[0])\n const durs = hgetallPairsToObject(raw[1])\n const fieldKeys = Object.keys(counts)\n for (const field of fieldKeys) {\n const count = parseInt(counts[field], 10)\n if (!count || count < 1) {\n continue\n }\n const dur = parseInt(durs[field] || '0', 10) || 0\n const parts = field.split(FIELD_SEP)\n let labels = null\n if (parts.length === 3) {\n const [m, route, statusStr] = parts\n labels = { method: m, route, status_code: statusStr }\n } else if (parts.length === 5 || parts.length === 6) {\n const [m, route, statusStr] = parts\n labels = { method: m, route, status_code: statusStr }\n }\n if (!labels) {\n continue\n }\n applyCount(labels, count)\n if (dur > 0) {\n applyDuration(labels, dur)\n }\n }\n resolve(true)\n } catch (e) {\n console.error(\n '[HttpMetricsRedisStore] flush apply failed:',\n e.message\n )\n resolve(false)\n }\n }\n )\n })\n }\n}\n\nmodule.exports = {\n HttpMetricsRedisStore,\n buildFieldKey,\n FIELD_SEP,\n DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -80,16 +80,12 @@ export class MetricsClient extends BaseMetricsClient {
|
|
|
80
80
|
* @param {string} params.method HTTP method
|
|
81
81
|
* @param {string} params.route Route or path pattern
|
|
82
82
|
* @param {number} params.status_code HTTP status code
|
|
83
|
-
* @param {string} [params.appId]
|
|
84
|
-
* @param {string} [params.databaseId]
|
|
85
83
|
* @param {number} params.duration Duration in milliseconds
|
|
86
84
|
*/
|
|
87
|
-
trackHttpRequest({ method, route, status_code,
|
|
85
|
+
trackHttpRequest({ method, route, status_code, duration }: {
|
|
88
86
|
method: string;
|
|
89
87
|
route: string;
|
|
90
88
|
status_code: number;
|
|
91
|
-
appId?: string | undefined;
|
|
92
|
-
databaseId?: string | undefined;
|
|
93
89
|
duration: number;
|
|
94
90
|
}): void;
|
|
95
91
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metricsClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsClient.js"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH;IACE;;;;;;;;;;;;;;;OAeG;IACH;;;;;;;;;;;;;;mBAYC;IATC,4BAGiD;IAEjD,yBAAyB;IACzB,uBAAgC;IAKlC;;;OAGG;IACH,
|
|
1
|
+
{"version":3,"file":"metricsClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsClient.js"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH;IACE;;;;;;;;;;;;;;;OAeG;IACH;;;;;;;;;;;;;;mBAYC;IATC,4BAGiD;IAEjD,yBAAyB;IACzB,uBAAgC;IAKlC;;;OAGG;IACH,4BA4DC;IAED;;;OAGG;IACH,0BAFa,MAAM,CA2BlB;IAED;;;OAGG;IACH,oBAFa,MAAM,CAiBlB;IAED;;;OAGG;IACH,2BAFa,MAAM,CAWlB;IAED;;;OAGG;IACH,2BAFa,MAAM,CAgBlB;IAED;;;OAGG;IACH,cAFa,QAAQ,MAAM,CAAC,CAO3B;IAED;;;;;;;;OAQG;IACH;QAL0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACS,QAAQ,EAAvB,MAAM;aAkBhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAsBvC;CACF"}
|
|
@@ -80,13 +80,13 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
80
80
|
this.createCounter({
|
|
81
81
|
name: 'app_requests_total',
|
|
82
82
|
help: 'Total number of HTTP requests',
|
|
83
|
-
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', '
|
|
83
|
+
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', 'status_code']),
|
|
84
84
|
useLabelsWithoutDynoId: true
|
|
85
85
|
});
|
|
86
86
|
this.createCounter({
|
|
87
87
|
name: 'app_requests_total_duration',
|
|
88
88
|
help: 'Total duration of HTTP requests in milliseconds',
|
|
89
|
-
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', '
|
|
89
|
+
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', 'status_code']),
|
|
90
90
|
useLabelsWithoutDynoId: true
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -186,32 +186,24 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
186
186
|
* @param {string} params.method HTTP method
|
|
187
187
|
* @param {string} params.route Route or path pattern
|
|
188
188
|
* @param {number} params.status_code HTTP status code
|
|
189
|
-
* @param {string} [params.appId]
|
|
190
|
-
* @param {string} [params.databaseId]
|
|
191
189
|
* @param {number} params.duration Duration in milliseconds
|
|
192
190
|
*/
|
|
193
191
|
trackHttpRequest({
|
|
194
192
|
method,
|
|
195
193
|
route,
|
|
196
194
|
status_code,
|
|
197
|
-
appId = '',
|
|
198
|
-
databaseId = '',
|
|
199
195
|
duration
|
|
200
196
|
}) {
|
|
201
197
|
if (!this.httpMetricsEnabled) return;
|
|
202
198
|
this.countersFunctions?.app_requests_total({
|
|
203
199
|
method,
|
|
204
200
|
route,
|
|
205
|
-
status_code
|
|
206
|
-
appId,
|
|
207
|
-
databaseId
|
|
201
|
+
status_code
|
|
208
202
|
});
|
|
209
203
|
this.countersFunctions?.app_requests_total_duration({
|
|
210
204
|
method,
|
|
211
205
|
route,
|
|
212
|
-
status_code
|
|
213
|
-
appId,
|
|
214
|
-
databaseId
|
|
206
|
+
status_code
|
|
215
207
|
}, duration);
|
|
216
208
|
}
|
|
217
209
|
|
|
@@ -231,14 +223,10 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
231
223
|
const start = Date.now();
|
|
232
224
|
res.on('finish', () => {
|
|
233
225
|
const route = req.route?.path || req.path || 'unknown';
|
|
234
|
-
const appId = req.params?.appId || req.body?.appId || req.query?.appId || '';
|
|
235
|
-
const databaseId = req.params?.databaseId || req.body?.databaseId || req.query?.databaseId || req.params?.datasourceId || req.body?.datasourceId || req.query?.datasourceId || '';
|
|
236
226
|
this.trackHttpRequest({
|
|
237
227
|
method: req.method,
|
|
238
228
|
route,
|
|
239
229
|
status_code: res.statusCode,
|
|
240
|
-
appId,
|
|
241
|
-
databaseId,
|
|
242
230
|
duration: Date.now() - start
|
|
243
231
|
});
|
|
244
232
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metricsClient.js","names":["fs","require","os","BaseMetricsClient","MetricsClient","constructor","config","httpMetricsEnabled","undefined","process","env","METRICS_HTTP_ENABLED","_lastUsageMicros","_lastCheckTime","Date","now","_initDefaultMetrics","createGauge","name","help","updateFn","getCpuUsagePercent","getAvailableCPUs","getContainerMemoryUsage","measureLag","getContainerMemoryLimit","uptime","createCounter","labelNames","withDefaultLabelsWithoutDynoId","useLabelsWithoutDynoId","stat","readFileSync","match","currentUsage","parseInt","deltaUsage","deltaTime","cpuMaxPath","existsSync","quotaStr","periodStr","trim","split","cpus","length","memoryUsage","rss","path","val","parsed","totalmem","Promise","resolve","start","setImmediate","trackHttpRequest","method","route","status_code","appId","databaseId","duration","countersFunctions","app_requests_total","app_requests_total_duration","trackHttpRequestMiddleware","req","res","next","enabled","on","params","body","query","datasourceId","statusCode","module","exports"],"sources":["../../src/metrics/metricsClient.js"],"sourcesContent":["const fs = require('fs')\nconst os = require('os')\nconst { BaseMetricsClient } = require('./baseMetricsClient')\n\n/**\n * MetricsClient handles Prometheus metrics collection and push.\n * Supports gauges, default process metrics, optional HTTP counters, and custom metrics.\n *\n * **HTTP metrics:** In-process counters only (`app_requests_*`), gated by `httpMetricsEnabled` /\n * `METRICS_HTTP_ENABLED`. For Redis-backed HTTP aggregation (multi-web / cluster), use\n * {@link HttpMetricsRedisRecorder} and {@link HttpMetricsRedisCollector} — not this class.\n *\n * @extends BaseMetricsClient\n */\nclass MetricsClient extends BaseMetricsClient {\n /**\n * @param {Object} [config]\n * @param {string} [config.appName] Name of the application\n * @param {string} [config.dynoId] Dyno/instance ID\n * @param {string} [config.processType] Process type (web, worker, etc.)\n * @param {boolean} [config.enabled] Enable metrics collection\n * @param {boolean} [config.httpMetricsEnabled] Enable HTTP request metrics (`app_requests_total`, `app_requests_total_duration`); defaults from `METRICS_HTTP_ENABLED === 'true'`\n * @param {boolean} [config.logValues] Log metrics values to console\n * @param {string} [config.pushgatewayUrl] Push URL (VM-agent import endpoint, e.g. .../api/v1/import/prometheus). /metrics is for GET (scrape), not POST (push).\n * @param {string} [config.pushgatewaySecret] Basic auth secret (Base64 of `user:password`)\n * @param {number} [config.intervalSec] Interval in seconds for pushing metrics\n * @param {boolean} [config.removeOldMetrics] Enable to clear metrics by service name\n * @param {function} [config.startupValidation] Add to validate on start push\n * @param {boolean} [config.disablePushgateway] Disable pushing to VM-agent (use HTTP scraping instead)\n * @param {boolean} [config.blockNodeDefaultMetrics] When true, skip prom-client default process metrics (rare; see {@link BaseMetricsClient})\n */\n constructor(config = {}) {\n super(config)\n\n this.httpMetricsEnabled =\n config.httpMetricsEnabled !== undefined\n ? config.httpMetricsEnabled\n : process.env.METRICS_HTTP_ENABLED === 'true'\n\n this._lastUsageMicros = 0\n this._lastCheckTime = Date.now()\n\n this._initDefaultMetrics()\n }\n\n /**\n * Register all built-in default Gauges and Counters.\n * @private\n */\n _initDefaultMetrics = () => {\n this.createGauge({\n name: 'app_process_cpu_usage_percent',\n help: 'Current CPU usage of the Node.js process in percent',\n updateFn: this.getCpuUsagePercent,\n })\n\n this.createGauge({\n name: 'app_available_cpu_count',\n help: 'How many CPU cores are available to this process',\n updateFn: this.getAvailableCPUs,\n })\n\n this.createGauge({\n name: 'app_container_memory_usage_bytes',\n help: 'Current container RAM usage from cgroup',\n updateFn: this.getContainerMemoryUsage,\n })\n\n this.createGauge({\n name: 'app_event_loop_lag_ms',\n help: 'Estimated event loop lag in milliseconds',\n updateFn: this.measureLag,\n })\n\n this.createGauge({\n name: 'app_container_memory_limit_bytes',\n help: 'Max RAM available to container from cgroup (memory.max)',\n updateFn: this.getContainerMemoryLimit,\n })\n\n this.createGauge({\n name: 'app_uptime_seconds',\n help: 'How long the process has been running',\n updateFn: process.uptime,\n })\n\n if (this.httpMetricsEnabled) {\n this.createCounter({\n name: 'app_requests_total',\n help: 'Total number of HTTP requests',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n 'appId',\n 'databaseId',\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 'appId',\n 'databaseId',\n 'status_code',\n ]),\n useLabelsWithoutDynoId: true,\n })\n }\n }\n\n /**\n * Get CPU usage percent (cgroup-aware)\n * @returns {number}\n */\n getCpuUsagePercent = () => {\n try {\n const stat = fs.readFileSync('/sys/fs/cgroup/cpu.stat', 'utf-8')\n const match = stat.match(/usage_usec (\\d+)/)\n if (!match) return 0\n\n const now = Date.now()\n const currentUsage = parseInt(match[1], 10)\n\n if (this._lastUsageMicros === 0) {\n this._lastUsageMicros = currentUsage\n this._lastCheckTime = now\n return 0\n }\n\n const deltaUsage = currentUsage - this._lastUsageMicros\n const deltaTime = now - this._lastCheckTime\n\n this._lastUsageMicros = currentUsage\n this._lastCheckTime = now\n\n return (deltaUsage / (deltaTime * 1000)) * 100\n } catch {\n return 0\n }\n }\n\n /**\n * Available CPU cores (cgroup quota or `os.cpus().length`).\n * @returns {number}\n */\n getAvailableCPUs() {\n try {\n const cpuMaxPath = '/sys/fs/cgroup/cpu.max'\n if (fs.existsSync(cpuMaxPath)) {\n const [quotaStr, periodStr] = fs\n .readFileSync(cpuMaxPath, 'utf8')\n .trim()\n .split(' ')\n if (quotaStr === 'max') return os.cpus().length\n return parseInt(quotaStr, 10) / parseInt(periodStr, 10)\n }\n return os.cpus().length\n } catch {\n return 1\n }\n }\n\n /**\n * Container memory usage in bytes (`memory.current` or RSS fallback).\n * @returns {number}\n */\n getContainerMemoryUsage() {\n try {\n return parseInt(\n fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf-8').trim(),\n 10\n )\n } catch {\n return process.memoryUsage().rss\n }\n }\n\n /**\n * Container memory limit in bytes (`memory.max` or host total).\n * @returns {number}\n */\n getContainerMemoryLimit() {\n try {\n const path = '/sys/fs/cgroup/memory.max'\n if (fs.existsSync(path)) {\n const val = fs.readFileSync(path, 'utf-8').trim()\n if (val !== 'max') {\n const parsed = parseInt(val, 10)\n if (parsed && parsed < os.totalmem()) return parsed\n }\n }\n return os.totalmem()\n } catch {\n return os.totalmem()\n }\n }\n\n /**\n * Event loop lag sample in milliseconds.\n * @returns {Promise<number>}\n */\n measureLag() {\n return new Promise(resolve => {\n const start = Date.now()\n setImmediate(() => resolve(Date.now() - start))\n })\n }\n\n /**\n * Increment HTTP request counters (in-process). No-op if `httpMetricsEnabled` is false.\n *\n * @param {Object} params\n * @param {string} params.method HTTP method\n * @param {string} params.route Route or path pattern\n * @param {number} params.status_code HTTP status code\n * @param {string} [params.appId]\n * @param {string} [params.databaseId]\n * @param {number} params.duration Duration in milliseconds\n */\n trackHttpRequest({\n method,\n route,\n status_code,\n appId = '',\n databaseId = '',\n duration,\n }) {\n if (!this.httpMetricsEnabled) return\n\n this.countersFunctions?.app_requests_total({\n method,\n route,\n status_code,\n appId,\n databaseId,\n })\n this.countersFunctions?.app_requests_total_duration(\n {\n method,\n route,\n status_code,\n appId,\n databaseId,\n },\n duration\n )\n }\n\n /**\n * Express middleware: records `app_requests_*` on response finish.\n * Skips when disabled, HTTP metrics off, or `OPTIONS`.\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 (!this.enabled || !this.httpMetricsEnabled || 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 const appId =\n req.params?.appId || req.body?.appId || req.query?.appId || ''\n const databaseId =\n req.params?.databaseId ||\n req.body?.databaseId ||\n req.query?.databaseId ||\n req.params?.datasourceId ||\n req.body?.datasourceId ||\n req.query?.datasourceId ||\n ''\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n appId,\n databaseId,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { MetricsClient }\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"metricsClient.js","names":["fs","require","os","BaseMetricsClient","MetricsClient","constructor","config","httpMetricsEnabled","undefined","process","env","METRICS_HTTP_ENABLED","_lastUsageMicros","_lastCheckTime","Date","now","_initDefaultMetrics","createGauge","name","help","updateFn","getCpuUsagePercent","getAvailableCPUs","getContainerMemoryUsage","measureLag","getContainerMemoryLimit","uptime","createCounter","labelNames","withDefaultLabelsWithoutDynoId","useLabelsWithoutDynoId","stat","readFileSync","match","currentUsage","parseInt","deltaUsage","deltaTime","cpuMaxPath","existsSync","quotaStr","periodStr","trim","split","cpus","length","memoryUsage","rss","path","val","parsed","totalmem","Promise","resolve","start","setImmediate","trackHttpRequest","method","route","status_code","duration","countersFunctions","app_requests_total","app_requests_total_duration","trackHttpRequestMiddleware","req","res","next","enabled","on","statusCode","module","exports"],"sources":["../../src/metrics/metricsClient.js"],"sourcesContent":["const fs = require('fs')\nconst os = require('os')\nconst { BaseMetricsClient } = require('./baseMetricsClient')\n\n/**\n * MetricsClient handles Prometheus metrics collection and push.\n * Supports gauges, default process metrics, optional HTTP counters, and custom metrics.\n *\n * **HTTP metrics:** In-process counters only (`app_requests_*`), gated by `httpMetricsEnabled` /\n * `METRICS_HTTP_ENABLED`. For Redis-backed HTTP aggregation (multi-web / cluster), use\n * {@link HttpMetricsRedisRecorder} and {@link HttpMetricsRedisCollector} — not this class.\n *\n * @extends BaseMetricsClient\n */\nclass MetricsClient extends BaseMetricsClient {\n /**\n * @param {Object} [config]\n * @param {string} [config.appName] Name of the application\n * @param {string} [config.dynoId] Dyno/instance ID\n * @param {string} [config.processType] Process type (web, worker, etc.)\n * @param {boolean} [config.enabled] Enable metrics collection\n * @param {boolean} [config.httpMetricsEnabled] Enable HTTP request metrics (`app_requests_total`, `app_requests_total_duration`); defaults from `METRICS_HTTP_ENABLED === 'true'`\n * @param {boolean} [config.logValues] Log metrics values to console\n * @param {string} [config.pushgatewayUrl] Push URL (VM-agent import endpoint, e.g. .../api/v1/import/prometheus). /metrics is for GET (scrape), not POST (push).\n * @param {string} [config.pushgatewaySecret] Basic auth secret (Base64 of `user:password`)\n * @param {number} [config.intervalSec] Interval in seconds for pushing metrics\n * @param {boolean} [config.removeOldMetrics] Enable to clear metrics by service name\n * @param {function} [config.startupValidation] Add to validate on start push\n * @param {boolean} [config.disablePushgateway] Disable pushing to VM-agent (use HTTP scraping instead)\n * @param {boolean} [config.blockNodeDefaultMetrics] When true, skip prom-client default process metrics (rare; see {@link BaseMetricsClient})\n */\n constructor(config = {}) {\n super(config)\n\n this.httpMetricsEnabled =\n config.httpMetricsEnabled !== undefined\n ? config.httpMetricsEnabled\n : process.env.METRICS_HTTP_ENABLED === 'true'\n\n this._lastUsageMicros = 0\n this._lastCheckTime = Date.now()\n\n this._initDefaultMetrics()\n }\n\n /**\n * Register all built-in default Gauges and Counters.\n * @private\n */\n _initDefaultMetrics = () => {\n this.createGauge({\n name: 'app_process_cpu_usage_percent',\n help: 'Current CPU usage of the Node.js process in percent',\n updateFn: this.getCpuUsagePercent,\n })\n\n this.createGauge({\n name: 'app_available_cpu_count',\n help: 'How many CPU cores are available to this process',\n updateFn: this.getAvailableCPUs,\n })\n\n this.createGauge({\n name: 'app_container_memory_usage_bytes',\n help: 'Current container RAM usage from cgroup',\n updateFn: this.getContainerMemoryUsage,\n })\n\n this.createGauge({\n name: 'app_event_loop_lag_ms',\n help: 'Estimated event loop lag in milliseconds',\n updateFn: this.measureLag,\n })\n\n this.createGauge({\n name: 'app_container_memory_limit_bytes',\n help: 'Max RAM available to container from cgroup (memory.max)',\n updateFn: this.getContainerMemoryLimit,\n })\n\n this.createGauge({\n name: 'app_uptime_seconds',\n help: 'How long the process has been running',\n updateFn: process.uptime,\n })\n\n if (this.httpMetricsEnabled) {\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 /**\n * Get CPU usage percent (cgroup-aware)\n * @returns {number}\n */\n getCpuUsagePercent = () => {\n try {\n const stat = fs.readFileSync('/sys/fs/cgroup/cpu.stat', 'utf-8')\n const match = stat.match(/usage_usec (\\d+)/)\n if (!match) return 0\n\n const now = Date.now()\n const currentUsage = parseInt(match[1], 10)\n\n if (this._lastUsageMicros === 0) {\n this._lastUsageMicros = currentUsage\n this._lastCheckTime = now\n return 0\n }\n\n const deltaUsage = currentUsage - this._lastUsageMicros\n const deltaTime = now - this._lastCheckTime\n\n this._lastUsageMicros = currentUsage\n this._lastCheckTime = now\n\n return (deltaUsage / (deltaTime * 1000)) * 100\n } catch {\n return 0\n }\n }\n\n /**\n * Available CPU cores (cgroup quota or `os.cpus().length`).\n * @returns {number}\n */\n getAvailableCPUs() {\n try {\n const cpuMaxPath = '/sys/fs/cgroup/cpu.max'\n if (fs.existsSync(cpuMaxPath)) {\n const [quotaStr, periodStr] = fs\n .readFileSync(cpuMaxPath, 'utf8')\n .trim()\n .split(' ')\n if (quotaStr === 'max') return os.cpus().length\n return parseInt(quotaStr, 10) / parseInt(periodStr, 10)\n }\n return os.cpus().length\n } catch {\n return 1\n }\n }\n\n /**\n * Container memory usage in bytes (`memory.current` or RSS fallback).\n * @returns {number}\n */\n getContainerMemoryUsage() {\n try {\n return parseInt(\n fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf-8').trim(),\n 10\n )\n } catch {\n return process.memoryUsage().rss\n }\n }\n\n /**\n * Container memory limit in bytes (`memory.max` or host total).\n * @returns {number}\n */\n getContainerMemoryLimit() {\n try {\n const path = '/sys/fs/cgroup/memory.max'\n if (fs.existsSync(path)) {\n const val = fs.readFileSync(path, 'utf-8').trim()\n if (val !== 'max') {\n const parsed = parseInt(val, 10)\n if (parsed && parsed < os.totalmem()) return parsed\n }\n }\n return os.totalmem()\n } catch {\n return os.totalmem()\n }\n }\n\n /**\n * Event loop lag sample in milliseconds.\n * @returns {Promise<number>}\n */\n measureLag() {\n return new Promise(resolve => {\n const start = Date.now()\n setImmediate(() => resolve(Date.now() - start))\n })\n }\n\n /**\n * Increment HTTP request counters (in-process). No-op if `httpMetricsEnabled` is false.\n *\n * @param {Object} params\n * @param {string} params.method HTTP method\n * @param {string} params.route Route or path pattern\n * @param {number} params.status_code HTTP status code\n * @param {number} params.duration Duration in milliseconds\n */\n trackHttpRequest({ method, route, status_code, duration }) {\n if (!this.httpMetricsEnabled) return\n\n this.countersFunctions?.app_requests_total({\n method,\n route,\n status_code,\n })\n this.countersFunctions?.app_requests_total_duration(\n {\n method,\n route,\n status_code,\n },\n duration\n )\n }\n\n /**\n * Express middleware: records `app_requests_*` on response finish.\n * Skips when disabled, HTTP metrics off, or `OPTIONS`.\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 (!this.enabled || !this.httpMetricsEnabled || 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 = { MetricsClient }\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -59,8 +59,6 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
59
59
|
labelNames: this.withDefaultLabelsWithoutDynoId([
|
|
60
60
|
'method',
|
|
61
61
|
'route',
|
|
62
|
-
'appId',
|
|
63
|
-
'databaseId',
|
|
64
62
|
'status_code',
|
|
65
63
|
]),
|
|
66
64
|
useLabelsWithoutDynoId: true,
|
|
@@ -72,8 +70,6 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
72
70
|
labelNames: this.withDefaultLabelsWithoutDynoId([
|
|
73
71
|
'method',
|
|
74
72
|
'route',
|
|
75
|
-
'appId',
|
|
76
|
-
'databaseId',
|
|
77
73
|
'status_code',
|
|
78
74
|
]),
|
|
79
75
|
useLabelsWithoutDynoId: true,
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
const { HttpMetricsRedisStore
|
|
2
|
-
|
|
3
|
-
function trunc(s, max = 120) {
|
|
4
|
-
const t = String(s)
|
|
5
|
-
return t.length > max ? `${t.slice(0, max - 3)}...` : t
|
|
6
|
-
}
|
|
1
|
+
const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
@@ -39,25 +34,10 @@ class HttpMetricsRedisRecorder {
|
|
|
39
34
|
* @param {string} params.method
|
|
40
35
|
* @param {string} params.route
|
|
41
36
|
* @param {number} params.status_code
|
|
42
|
-
* @param {string} [params.appId]
|
|
43
|
-
* @param {string} [params.databaseId]
|
|
44
37
|
* @param {number} params.duration
|
|
45
38
|
*/
|
|
46
|
-
trackHttpRequest({
|
|
47
|
-
method,
|
|
48
|
-
route,
|
|
49
|
-
status_code,
|
|
50
|
-
appId = '',
|
|
51
|
-
databaseId = '',
|
|
52
|
-
duration,
|
|
53
|
-
}) {
|
|
54
|
-
httpMetricsTraceLog(
|
|
55
|
-
`track_request pid=${process.pid} app=${this.appName} segment=${this.processType} ` +
|
|
56
|
-
`method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` +
|
|
57
|
-
`appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)} ` +
|
|
58
|
-
`(then save_to_redis lines from store)`
|
|
59
|
-
)
|
|
60
|
-
this._store.record(method, route, status_code, appId, databaseId, duration)
|
|
39
|
+
trackHttpRequest({ method, route, status_code, duration }) {
|
|
40
|
+
this._store.record(method, route, status_code, duration)
|
|
61
41
|
}
|
|
62
42
|
|
|
63
43
|
/**
|
|
@@ -77,23 +57,11 @@ class HttpMetricsRedisRecorder {
|
|
|
77
57
|
const start = Date.now()
|
|
78
58
|
res.on('finish', () => {
|
|
79
59
|
const route = req.route?.path || req.path || 'unknown'
|
|
80
|
-
const appId =
|
|
81
|
-
req.params?.appId || req.body?.appId || req.query?.appId || ''
|
|
82
|
-
const databaseId =
|
|
83
|
-
req.params?.databaseId ||
|
|
84
|
-
req.body?.databaseId ||
|
|
85
|
-
req.query?.databaseId ||
|
|
86
|
-
req.params?.datasourceId ||
|
|
87
|
-
req.body?.datasourceId ||
|
|
88
|
-
req.query?.datasourceId ||
|
|
89
|
-
''
|
|
90
60
|
|
|
91
61
|
this.trackHttpRequest({
|
|
92
62
|
method: req.method,
|
|
93
63
|
route,
|
|
94
64
|
status_code: res.statusCode,
|
|
95
|
-
appId,
|
|
96
|
-
databaseId,
|
|
97
65
|
duration: Date.now() - start,
|
|
98
66
|
})
|
|
99
67
|
})
|
|
@@ -10,41 +10,6 @@ const FIELD_SEP = '\x1e'
|
|
|
10
10
|
*/
|
|
11
11
|
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120
|
|
12
12
|
|
|
13
|
-
const LOG = '[http-metrics-redis]'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* When Node runs behind `cluster`, HTTP is usually served from workers — include worker id in traces.
|
|
17
|
-
* @returns {string}
|
|
18
|
-
*/
|
|
19
|
-
function clusterHint() {
|
|
20
|
-
try {
|
|
21
|
-
// eslint-disable-next-line global-require
|
|
22
|
-
const c = require('cluster')
|
|
23
|
-
if (c.isWorker && c.worker != null) {
|
|
24
|
-
return ` cluster_worker=${c.worker.id}`
|
|
25
|
-
}
|
|
26
|
-
} catch (_) {
|
|
27
|
-
/* cluster not in use */
|
|
28
|
-
}
|
|
29
|
-
return ''
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function truncField(s, max = 96) {
|
|
33
|
-
const t = String(s)
|
|
34
|
-
return t.length > max ? `${t.slice(0, max - 3)}...` : t
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
39
|
-
* Single `console.log` only: many hosts (e.g. Heroku) merge stdout+stderr into one stream and would
|
|
40
|
-
* show duplicate lines if we also wrote the same text to stderr.
|
|
41
|
-
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
42
|
-
*/
|
|
43
|
-
function httpMetricsTraceLog(body) {
|
|
44
|
-
const line = `${LOG} ${body}${clusterHint()}`
|
|
45
|
-
console.log(line)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
13
|
const DRAIN_LUA = `
|
|
49
14
|
local function drain(key)
|
|
50
15
|
local v = redis.call('HGETALL', key)
|
|
@@ -54,28 +19,14 @@ end
|
|
|
54
19
|
return {drain(KEYS[1]), drain(KEYS[2])}
|
|
55
20
|
`
|
|
56
21
|
|
|
57
|
-
/**
|
|
58
|
-
* @returns {boolean} Whether the `redis` npm package is resolvable (optional peer).
|
|
59
|
-
*/
|
|
60
|
-
function isRedisPeerInstalled() {
|
|
61
|
-
try {
|
|
62
|
-
require.resolve('redis')
|
|
63
|
-
return true
|
|
64
|
-
} catch {
|
|
65
|
-
return false
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
22
|
/**
|
|
70
23
|
* @param {string} method
|
|
71
24
|
* @param {string} route
|
|
72
25
|
* @param {number} statusCode
|
|
73
|
-
* @param {string} appId
|
|
74
|
-
* @param {string} databaseId
|
|
75
26
|
* @returns {string}
|
|
76
27
|
*/
|
|
77
|
-
function buildFieldKey(method, route, statusCode
|
|
78
|
-
return [method, route, String(statusCode)
|
|
28
|
+
function buildFieldKey(method, route, statusCode) {
|
|
29
|
+
return [method, route, String(statusCode)].join(FIELD_SEP)
|
|
79
30
|
}
|
|
80
31
|
|
|
81
32
|
function hgetallPairsToObject(pairs) {
|
|
@@ -93,11 +44,8 @@ function hgetallPairsToObject(pairs) {
|
|
|
93
44
|
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
94
45
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.
|
|
95
46
|
*
|
|
96
|
-
* **Structure:** `countKey` / `durKey` are each **one Redis hash**.
|
|
97
|
-
*
|
|
98
|
-
* appId, databaseId)`. The **value** in `:count` is **HINCRBY 1** per request for that group; in `:dur`
|
|
99
|
-
* it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
|
|
100
|
-
* Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
|
|
47
|
+
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes
|
|
48
|
+
* `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.
|
|
101
49
|
*/
|
|
102
50
|
class HttpMetricsRedisStore {
|
|
103
51
|
/**
|
|
@@ -135,19 +83,13 @@ class HttpMetricsRedisStore {
|
|
|
135
83
|
* @param {string} method
|
|
136
84
|
* @param {string} route
|
|
137
85
|
* @param {number} statusCode
|
|
138
|
-
* @param {string} appId
|
|
139
|
-
* @param {string} databaseId
|
|
140
86
|
* @param {number} durationMs
|
|
141
87
|
*/
|
|
142
|
-
record(method, route, statusCode,
|
|
88
|
+
record(method, route, statusCode, durationMs) {
|
|
143
89
|
try {
|
|
144
90
|
const client = this._ensureClient()
|
|
145
|
-
const field = buildFieldKey(method, route, statusCode
|
|
91
|
+
const field = buildFieldKey(method, route, statusCode)
|
|
146
92
|
const dur = Math.max(0, Math.round(Number(durationMs) || 0))
|
|
147
|
-
httpMetricsTraceLog(
|
|
148
|
-
`save_to_redis pid=${process.pid} countKey=${this.countKey} ` +
|
|
149
|
-
`durKey=${this.durKey} field=${truncField(field)} durationMs=${dur} (before MULTI/EXEC)`
|
|
150
|
-
)
|
|
151
93
|
client
|
|
152
94
|
.multi()
|
|
153
95
|
.hincrby(this.countKey, field, 1)
|
|
@@ -157,11 +99,7 @@ class HttpMetricsRedisStore {
|
|
|
157
99
|
.exec(err => {
|
|
158
100
|
if (err) {
|
|
159
101
|
console.error('[HttpMetricsRedisStore] record failed:', err.message)
|
|
160
|
-
return
|
|
161
102
|
}
|
|
162
|
-
httpMetricsTraceLog(
|
|
163
|
-
`save_to_redis_ok pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} (after MULTI/EXEC)`
|
|
164
|
-
)
|
|
165
103
|
})
|
|
166
104
|
} catch (e) {
|
|
167
105
|
console.error('[HttpMetricsRedisStore] record:', e.message)
|
|
@@ -182,9 +120,6 @@ class HttpMetricsRedisStore {
|
|
|
182
120
|
return Promise.resolve(false)
|
|
183
121
|
}
|
|
184
122
|
return new Promise(resolve => {
|
|
185
|
-
httpMetricsTraceLog(
|
|
186
|
-
`get_from_redis_try pid=${process.pid} countKey=${this.countKey} (EVAL drain)`
|
|
187
|
-
)
|
|
188
123
|
client.eval(
|
|
189
124
|
DRAIN_LUA,
|
|
190
125
|
2,
|
|
@@ -202,17 +137,12 @@ class HttpMetricsRedisStore {
|
|
|
202
137
|
|
|
203
138
|
try {
|
|
204
139
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
205
|
-
httpMetricsTraceLog(
|
|
206
|
-
`get_from_redis_empty pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (lua returned no pairs)`
|
|
207
|
-
)
|
|
208
140
|
resolve(true)
|
|
209
141
|
return
|
|
210
142
|
}
|
|
211
143
|
const counts = hgetallPairsToObject(raw[0])
|
|
212
144
|
const durs = hgetallPairsToObject(raw[1])
|
|
213
145
|
const fieldKeys = Object.keys(counts)
|
|
214
|
-
let sumRequests = 0
|
|
215
|
-
const samples = []
|
|
216
146
|
for (const field of fieldKeys) {
|
|
217
147
|
const count = parseInt(counts[field], 10)
|
|
218
148
|
if (!count || count < 1) {
|
|
@@ -220,31 +150,22 @@ class HttpMetricsRedisStore {
|
|
|
220
150
|
}
|
|
221
151
|
const dur = parseInt(durs[field] || '0', 10) || 0
|
|
222
152
|
const parts = field.split(FIELD_SEP)
|
|
223
|
-
|
|
224
|
-
|
|
153
|
+
let labels = null
|
|
154
|
+
if (parts.length === 3) {
|
|
155
|
+
const [m, route, statusStr] = parts
|
|
156
|
+
labels = { method: m, route, status_code: statusStr }
|
|
157
|
+
} else if (parts.length === 5 || parts.length === 6) {
|
|
158
|
+
const [m, route, statusStr] = parts
|
|
159
|
+
labels = { method: m, route, status_code: statusStr }
|
|
225
160
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (samples.length < 3) {
|
|
229
|
-
samples.push(`${m} ${route} ${statusStr} x${count}`)
|
|
230
|
-
}
|
|
231
|
-
const labels = {
|
|
232
|
-
method: m,
|
|
233
|
-
route,
|
|
234
|
-
status_code: statusStr,
|
|
235
|
-
appId: aid,
|
|
236
|
-
databaseId: did,
|
|
161
|
+
if (!labels) {
|
|
162
|
+
continue
|
|
237
163
|
}
|
|
238
164
|
applyCount(labels, count)
|
|
239
165
|
if (dur > 0) {
|
|
240
166
|
applyDuration(labels, dur)
|
|
241
167
|
}
|
|
242
168
|
}
|
|
243
|
-
httpMetricsTraceLog(
|
|
244
|
-
`get_from_redis_ok pid=${process.pid} countKey=${this.countKey} hash_fields=${
|
|
245
|
-
fieldKeys.length
|
|
246
|
-
} sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`
|
|
247
|
-
)
|
|
248
169
|
resolve(true)
|
|
249
170
|
} catch (e) {
|
|
250
171
|
console.error(
|
|
@@ -263,7 +184,5 @@ module.exports = {
|
|
|
263
184
|
HttpMetricsRedisStore,
|
|
264
185
|
buildFieldKey,
|
|
265
186
|
FIELD_SEP,
|
|
266
|
-
isRedisPeerInstalled,
|
|
267
187
|
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,
|
|
268
|
-
httpMetricsTraceLog,
|
|
269
188
|
}
|
|
@@ -91,8 +91,6 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
91
91
|
labelNames: this.withDefaultLabelsWithoutDynoId([
|
|
92
92
|
'method',
|
|
93
93
|
'route',
|
|
94
|
-
'appId',
|
|
95
|
-
'databaseId',
|
|
96
94
|
'status_code',
|
|
97
95
|
]),
|
|
98
96
|
useLabelsWithoutDynoId: true,
|
|
@@ -104,8 +102,6 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
104
102
|
labelNames: this.withDefaultLabelsWithoutDynoId([
|
|
105
103
|
'method',
|
|
106
104
|
'route',
|
|
107
|
-
'appId',
|
|
108
|
-
'databaseId',
|
|
109
105
|
'status_code',
|
|
110
106
|
]),
|
|
111
107
|
useLabelsWithoutDynoId: true,
|
|
@@ -218,34 +214,21 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
218
214
|
* @param {string} params.method HTTP method
|
|
219
215
|
* @param {string} params.route Route or path pattern
|
|
220
216
|
* @param {number} params.status_code HTTP status code
|
|
221
|
-
* @param {string} [params.appId]
|
|
222
|
-
* @param {string} [params.databaseId]
|
|
223
217
|
* @param {number} params.duration Duration in milliseconds
|
|
224
218
|
*/
|
|
225
|
-
trackHttpRequest({
|
|
226
|
-
method,
|
|
227
|
-
route,
|
|
228
|
-
status_code,
|
|
229
|
-
appId = '',
|
|
230
|
-
databaseId = '',
|
|
231
|
-
duration,
|
|
232
|
-
}) {
|
|
219
|
+
trackHttpRequest({ method, route, status_code, duration }) {
|
|
233
220
|
if (!this.httpMetricsEnabled) return
|
|
234
221
|
|
|
235
222
|
this.countersFunctions?.app_requests_total({
|
|
236
223
|
method,
|
|
237
224
|
route,
|
|
238
225
|
status_code,
|
|
239
|
-
appId,
|
|
240
|
-
databaseId,
|
|
241
226
|
})
|
|
242
227
|
this.countersFunctions?.app_requests_total_duration(
|
|
243
228
|
{
|
|
244
229
|
method,
|
|
245
230
|
route,
|
|
246
231
|
status_code,
|
|
247
|
-
appId,
|
|
248
|
-
databaseId,
|
|
249
232
|
},
|
|
250
233
|
duration
|
|
251
234
|
)
|
|
@@ -268,23 +251,11 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
268
251
|
const start = Date.now()
|
|
269
252
|
res.on('finish', () => {
|
|
270
253
|
const route = req.route?.path || req.path || 'unknown'
|
|
271
|
-
const appId =
|
|
272
|
-
req.params?.appId || req.body?.appId || req.query?.appId || ''
|
|
273
|
-
const databaseId =
|
|
274
|
-
req.params?.databaseId ||
|
|
275
|
-
req.body?.databaseId ||
|
|
276
|
-
req.query?.databaseId ||
|
|
277
|
-
req.params?.datasourceId ||
|
|
278
|
-
req.body?.datasourceId ||
|
|
279
|
-
req.query?.datasourceId ||
|
|
280
|
-
''
|
|
281
254
|
|
|
282
255
|
this.trackHttpRequest({
|
|
283
256
|
method: req.method,
|
|
284
257
|
route,
|
|
285
258
|
status_code: res.statusCode,
|
|
286
|
-
appId,
|
|
287
|
-
databaseId,
|
|
288
259
|
duration: Date.now() - start,
|
|
289
260
|
})
|
|
290
261
|
})
|