@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 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 **`docs/http-metrics-redis.md`** for key segments and what is stored in Redis.
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, 'x', 'y')
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 parts with FIELD_SEP', () => {
91
- expect(buildFieldKey('GET', '/api', 200, 'app1', 'db1')).toBe(
92
- ['GET', '/api', '200', 'app1', 'db1'].join(FIELD_SEP)
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, '', '', 12)
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, 'a1', 'd1')
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, `app-${i % 4}`, `db-${i % 3}`, 10 + i)
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, 'a1', 'd1', 5)
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, '', '', 3)
327
- dynoB.record('GET', '/b', 304, '', '', 7)
328
- dynoA.record('POST', '/c', 201, 'x', 'y', 11)
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, `app${worker}`, 'db0', 2)
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, 'a', 'd', 1],
387
- ['GET', '/x', 404, 'a', 'd', 2],
388
- ['DELETE', '/x|y', 204, 'b', 'd', 3],
412
+ ['GET', '/x', 200, 1],
413
+ ['GET', '/x', 404, 2],
414
+ ['DELETE', '/x|y', 204, 3],
389
415
  ]
390
- for (const [m, r, s, a, d, dur] of samples) {
391
- store.record(m, r, s, a, d, dur)
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;;;;;;;;;;;;;;mBAiErC;IAhCC,8BAKE;CAgDL"}
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', 'appId', 'databaseId', 'status_code']),
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', 'appId', 'databaseId', 'status_code']),
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 '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 * 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,OAAO,EACP,YAAY,EACZ,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,OAAO,EACP,YAAY,EACZ,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":[]}
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, appId, databaseId, duration, }: {
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":"AAOA;;;;;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;;;;;;;;OAQG;IACH;QAP0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACU,KAAK;QACL,UAAU;QACX,QAAQ,EAAvB,MAAM;aAiBhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAkCvC;CACF"}
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
- httpMetricsTraceLog(`track_request pid=${process.pid} app=${this.appName} segment=${this.processType} ` + `method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` + `appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)} ` + `(then save_to_redis lines from store)`);
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","httpMetricsTraceLog","require","trunc","s","max","t","String","length","slice","HttpMetricsRedisRecorder","constructor","redisClient","appName","processType","ttlSec","Error","resolvedAppName","process","env","BUILD_APP_NAME","_store","trackHttpRequest","method","route","status_code","appId","databaseId","duration","pid","record","trackHttpRequestMiddleware","req","res","next","start","Date","now","on","path","params","body","query","datasourceId","statusCode","module","exports"],"sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"sourcesContent":["const { HttpMetricsRedisStore, httpMetricsTraceLog } = require('./httpMetricsRedisStore')\n\nfunction trunc(s, max = 120) {\n const t = String(s)\n return t.length > max ? `${t.slice(0, max - 3)}...` : t\n}\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 {string} [params.appId]\n * @param {string} [params.databaseId]\n * @param {number} params.duration\n */\n trackHttpRequest({\n method,\n route,\n status_code,\n appId = '',\n databaseId = '',\n duration,\n }) {\n httpMetricsTraceLog(\n `track_request pid=${process.pid} app=${this.appName} segment=${this.processType} ` +\n `method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` +\n `appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)} ` +\n `(then save_to_redis lines from store)`\n )\n this._store.record(method, route, status_code, appId, databaseId, 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 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 = { HttpMetricsRedisRecorder }\n"],"mappings":"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","ignoreList":[]}
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**. The hash has **many fields** — not
6
- * one bucket for all traffic. Each **field name** encodes one label group `(method, route, status_code,
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, appId: string, databaseId: string, durationMs: number): void;
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, appId: string, databaseId: string): string;
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":"AA2FA;;;;;;;;;GASG;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;;;;;;;OAOG;IACH,eAPW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,cACN,MAAM,QA6BhB;IAED;;;;OAIG;IACH,qCAJoB,MAAM,SAAS,MAAM,KAAK,IAAI,0BAC9B,MAAM,SAAS,MAAM,KAAK,IAAI,GACrC,QAAQ,OAAO,CAAC,CAqF5B;CACF;AA/LD;;;;;;;GAOG;AACH,sCAPW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AA9ED;;;GAGG;AACH,wBAFU,MAAM,CAEQ;AAoDxB;;GAEG;AACH,wCAFa,OAAO,CASnB;AA5DD;;;GAGG;AACH,iDAFU,MAAM,CAE8B;AA0B9C;;;;;GAKG;AACH,0CAFW,MAAM,QAKhB"}
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, appId, databaseId) {
77
- return [method, route, String(statusCode), appId, databaseId].join(FIELD_SEP);
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**. The hash has **many fields** — not
95
- * one bucket for all traffic. Each **field name** encodes one label group `(method, route, status_code,
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, appId, databaseId, durationMs) {
88
+ record(method, route, statusCode, durationMs) {
141
89
  try {
142
90
  const client = this._ensureClient();
143
- const field = buildFieldKey(method, route, statusCode, appId, databaseId);
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
- if (parts.length !== 5) {
198
- continue;
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
- const [m, route, statusStr, aid, did] = parts;
201
- sumRequests += count;
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
- isRedisPeerInstalled,
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":"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","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, appId, databaseId, duration, }: {
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,4BAgEC;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;;;;;;;;;;OAUG;IACH;QAP0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACU,KAAK;QACL,UAAU;QACX,QAAQ,EAAvB,MAAM;aA6BhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAkCvC;CACF"}
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', 'appId', 'databaseId', 'status_code']),
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', 'appId', 'databaseId', 'status_code']),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adalo/metrics",
3
- "version": "0.0.0-staging.27",
3
+ "version": "0.0.0-staging.29",
4
4
  "description": "Reusable metrics utilities for Node.js and Laravel apps",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -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, httpMetricsTraceLog } = require('./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, appId, databaseId) {
78
- return [method, route, String(statusCode), appId, databaseId].join(FIELD_SEP)
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**. The hash has **many fields** — not
97
- * one bucket for all traffic. Each **field name** encodes one label group `(method, route, status_code,
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, appId, databaseId, durationMs) {
88
+ record(method, route, statusCode, durationMs) {
143
89
  try {
144
90
  const client = this._ensureClient()
145
- const field = buildFieldKey(method, route, statusCode, appId, databaseId)
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
- if (parts.length !== 5) {
224
- continue
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
- const [m, route, statusStr, aid, did] = parts
227
- sumRequests += count
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
  })