@adalo/metrics 0.0.0-staging.27 → 0.0.0-staging.28

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.
@@ -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, '', '', '99')
39
39
  redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
40
40
  cb(null, [
41
41
  [field, '1'],
@@ -44,7 +44,7 @@ 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, 'x', 'y', String(process.pid))
48
48
 
49
49
  rec.trackHttpRequest({
50
50
  method: 'GET',
@@ -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 parts with FIELD_SEP (includes pid)', () => {
91
+ expect(buildFieldKey('GET', '/api', 200, 'app1', 'db1', '94662')).toBe(
92
+ ['GET', '/api', '200', 'app1', 'db1', '94662'].join(FIELD_SEP)
93
93
  )
94
94
  })
95
95
  })
@@ -123,7 +123,7 @@ 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, '', '', String(process.pid))
127
127
 
128
128
  store.record('GET', '/x', 200, '', '', 12)
129
129
  await flushMicrotasks()
@@ -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, '', '', '111')
144
144
  redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
145
145
  cb(null, [
146
146
  [field, '100'],
@@ -166,6 +166,7 @@ describe('HttpMetricsRedisStore', () => {
166
166
  status_code: '200',
167
167
  appId: '',
168
168
  databaseId: '',
169
+ pid: '111',
169
170
  },
170
171
  100
171
172
  )
@@ -176,6 +177,7 @@ describe('HttpMetricsRedisStore', () => {
176
177
  status_code: '200',
177
178
  appId: '',
178
179
  databaseId: '',
180
+ pid: '111',
179
181
  },
180
182
  4500
181
183
  )
@@ -183,7 +185,7 @@ describe('HttpMetricsRedisStore', () => {
183
185
 
184
186
  it('drains hashes and applies count and duration', async () => {
185
187
  const redis = createRedisV3Mock()
186
- const field = buildFieldKey('POST', '/r', 201, 'a1', 'd1')
188
+ const field = buildFieldKey('POST', '/r', 201, 'a1', 'd1', '222')
187
189
  redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
188
190
  cb(null, [
189
191
  [field, '2'],
@@ -216,6 +218,7 @@ describe('HttpMetricsRedisStore', () => {
216
218
  status_code: '201',
217
219
  appId: 'a1',
218
220
  databaseId: 'd1',
221
+ pid: '222',
219
222
  },
220
223
  2
221
224
  )
@@ -226,11 +229,40 @@ describe('HttpMetricsRedisStore', () => {
226
229
  status_code: '201',
227
230
  appId: 'a1',
228
231
  databaseId: 'd1',
232
+ pid: '222',
229
233
  },
230
234
  50
231
235
  )
232
236
  })
233
237
 
238
+ it('legacy 5-part hash fields get pid=legacy', async () => {
239
+ const redis = createRedisV3Mock()
240
+ const legacyField = ['GET', '/old', '200', '', ''].join(FIELD_SEP)
241
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
242
+ cb(null, [
243
+ [legacyField, '3'],
244
+ [legacyField, '9'],
245
+ ])
246
+ })
247
+ const store = new HttpMetricsRedisStore({
248
+ redisClient: redis,
249
+ appName: 'app',
250
+ processType: 'web',
251
+ })
252
+ const applyCount = jest.fn()
253
+ const applyDuration = jest.fn()
254
+ const ok = await store.flushToCounters(applyCount, applyDuration)
255
+ expect(ok).toBe(true)
256
+ expect(applyCount).toHaveBeenCalledWith(
257
+ expect.objectContaining({ pid: 'legacy' }),
258
+ 3
259
+ )
260
+ expect(applyDuration).toHaveBeenCalledWith(
261
+ expect.objectContaining({ pid: 'legacy' }),
262
+ 9
263
+ )
264
+ })
265
+
234
266
  it('resolves true with no applies when eval returns short array', async () => {
235
267
  const redis = createRedisV3Mock()
236
268
  redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
@@ -308,11 +340,12 @@ describe('HttpMetricsRedisStore', () => {
308
340
  status_code: '200',
309
341
  appId: 'a1',
310
342
  databaseId: 'd1',
343
+ pid: String(process.pid),
311
344
  }),
312
345
  n
313
346
  )
314
347
  expect(applyDuration).toHaveBeenCalledWith(
315
- expect.objectContaining({ route: '/hot' }),
348
+ expect.objectContaining({ route: '/hot', pid: String(process.pid) }),
316
349
  n * 5
317
350
  )
318
351
  })
@@ -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;;;;;;;;;;;;;;mBAmErC;IAlCC,8BAKE;CAkDL"}
@@ -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', 'appId', 'databaseId', 'status_code', 'pid']),
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', 'appId', 'databaseId', 'status_code', 'pid']),
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 'appId',\n 'databaseId',\n 'status_code',\n 'pid',\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 'pid',\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,EACb,KAAK,CACN,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,EACb,KAAK,CACN,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":[]}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * **Structure:** `countKey` / `durKey` are each **one Redis hash**. The hash has **many fields** — not
6
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`
7
+ * appId, databaseId, pid)`. The **value** in `:count` is **HINCRBY 1** per request for that group; in `:dur`
8
8
  * it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
9
9
  * Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
10
10
  */
@@ -53,9 +53,10 @@ export class HttpMetricsRedisStore {
53
53
  * @param {number} statusCode
54
54
  * @param {string} appId
55
55
  * @param {string} databaseId
56
+ * @param {string} pid - OS process id (string); distinguishes cluster workers / processes in one Redis hash
56
57
  * @returns {string}
57
58
  */
58
- export function buildFieldKey(method: string, route: string, statusCode: number, appId: string, databaseId: string): string;
59
+ export function buildFieldKey(method: string, route: string, statusCode: number, appId: string, databaseId: string, pid: string): string;
59
60
  /**
60
61
  * Record separator for hash fields (avoids collisions when route contains "|").
61
62
  * @type {string}
@@ -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":"AA8FA;;;;;;;;;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,QAoChB;IAED;;;;OAIG;IACH,qCAJoB,MAAM,SAAS,MAAM,KAAK,IAAI,0BAC9B,MAAM,SAAS,MAAM,KAAK,IAAI,GACrC,QAAQ,OAAO,CAAC,CAkG5B;CACF;AAtND;;;;;;;;GAQG;AACH,sCARW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,OACN,MAAM,GACJ,MAAM,CAMlB;AAjFD;;;GAGG;AACH,wBAFU,MAAM,CAEQ;AAoDxB;;GAEG;AACH,wCAFa,OAAO,CASnB;AA5DD;;;GAGG;AACH,iDAFU,MAAM,CAE8B;AA0B9C;;;;;GAKG;AACH,0CAFW,MAAM,QAKhB"}
@@ -71,10 +71,11 @@ function isRedisPeerInstalled() {
71
71
  * @param {number} statusCode
72
72
  * @param {string} appId
73
73
  * @param {string} databaseId
74
+ * @param {string} pid - OS process id (string); distinguishes cluster workers / processes in one Redis hash
74
75
  * @returns {string}
75
76
  */
76
- function buildFieldKey(method, route, statusCode, appId, databaseId) {
77
- return [method, route, String(statusCode), appId, databaseId].join(FIELD_SEP);
77
+ function buildFieldKey(method, route, statusCode, appId, databaseId, pid) {
78
+ return [method, route, String(statusCode), appId, databaseId, String(pid)].join(FIELD_SEP);
78
79
  }
79
80
  function hgetallPairsToObject(pairs) {
80
81
  const o = {};
@@ -93,7 +94,7 @@ function hgetallPairsToObject(pairs) {
93
94
  *
94
95
  * **Structure:** `countKey` / `durKey` are each **one Redis hash**. The hash has **many fields** — not
95
96
  * 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
+ * appId, databaseId, pid)`. The **value** in `:count` is **HINCRBY 1** per request for that group; in `:dur`
97
98
  * it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
98
99
  * Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
99
100
  */
@@ -140,7 +141,7 @@ class HttpMetricsRedisStore {
140
141
  record(method, route, statusCode, appId, databaseId, durationMs) {
141
142
  try {
142
143
  const client = this._ensureClient();
143
- const field = buildFieldKey(method, route, statusCode, appId, databaseId);
144
+ const field = buildFieldKey(method, route, statusCode, appId, databaseId, process.pid);
144
145
  const dur = Math.max(0, Math.round(Number(durationMs) || 0));
145
146
  httpMetricsTraceLog(`save_to_redis pid=${process.pid} countKey=${this.countKey} ` + `durKey=${this.durKey} field=${truncField(field)} durationMs=${dur} (before MULTI/EXEC)`);
146
147
  client.multi().hincrby(this.countKey, field, 1).hincrby(this.durKey, field, dur).expire(this.countKey, this.ttlSec).expire(this.durKey, this.ttlSec).exec(err => {
@@ -194,20 +195,33 @@ class HttpMetricsRedisStore {
194
195
  }
195
196
  const dur = parseInt(durs[field] || '0', 10) || 0;
196
197
  const parts = field.split(FIELD_SEP);
197
- if (parts.length !== 5) {
198
+ let m;
199
+ let route;
200
+ let statusStr;
201
+ let aid;
202
+ let did;
203
+ let pidStr;
204
+ if (parts.length === 6) {
205
+ ;
206
+ [m, route, statusStr, aid, did, pidStr] = parts;
207
+ } else if (parts.length === 5) {
208
+ ;
209
+ [m, route, statusStr, aid, did] = parts;
210
+ pidStr = 'legacy';
211
+ } else {
198
212
  continue;
199
213
  }
200
- const [m, route, statusStr, aid, did] = parts;
201
214
  sumRequests += count;
202
215
  if (samples.length < 3) {
203
- samples.push(`${m} ${route} ${statusStr} x${count}`);
216
+ samples.push(`${m} ${route} ${statusStr} pid=${pidStr} x${count}`);
204
217
  }
205
218
  const labels = {
206
219
  method: m,
207
220
  route,
208
221
  status_code: statusStr,
209
222
  appId: aid,
210
- databaseId: did
223
+ databaseId: did,
224
+ pid: pidStr
211
225
  };
212
226
  applyCount(labels, count);
213
227
  if (dur > 0) {
@@ -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","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","pid","join","hgetallPairsToObject","pairs","o","i","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","_ensureClient","record","durationMs","client","field","process","dur","Math","round","Number","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","pidStr","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 * @param {string} pid - OS process id (string); distinguishes cluster workers / processes in one Redis hash\n * @returns {string}\n */\nfunction buildFieldKey(method, route, statusCode, appId, databaseId, pid) {\n return [method, route, String(statusCode), appId, databaseId, String(pid)].join(\n FIELD_SEP\n )\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, pid)`. 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(\n method,\n route,\n statusCode,\n appId,\n databaseId,\n process.pid\n )\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 let m\n let route\n let statusStr\n let aid\n let did\n let pidStr\n if (parts.length === 6) {\n ;[m, route, statusStr, aid, did, pidStr] = parts\n } else if (parts.length === 5) {\n ;[m, route, statusStr, aid, did] = parts\n pidStr = 'legacy'\n } else {\n continue\n }\n sumRequests += count\n if (samples.length < 3) {\n samples.push(\n `${m} ${route} ${statusStr} pid=${pidStr} x${count}`\n )\n }\n const labels = {\n method: m,\n route,\n status_code: statusStr,\n appId: aid,\n databaseId: did,\n pid: pidStr,\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":[]}
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.28",
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",
@@ -62,6 +62,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
62
62
  'appId',
63
63
  'databaseId',
64
64
  'status_code',
65
+ 'pid',
65
66
  ]),
66
67
  useLabelsWithoutDynoId: true,
67
68
  })
@@ -75,6 +76,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
75
76
  'appId',
76
77
  'databaseId',
77
78
  'status_code',
79
+ 'pid',
78
80
  ]),
79
81
  useLabelsWithoutDynoId: true,
80
82
  })
@@ -72,10 +72,13 @@ function isRedisPeerInstalled() {
72
72
  * @param {number} statusCode
73
73
  * @param {string} appId
74
74
  * @param {string} databaseId
75
+ * @param {string} pid - OS process id (string); distinguishes cluster workers / processes in one Redis hash
75
76
  * @returns {string}
76
77
  */
77
- function buildFieldKey(method, route, statusCode, appId, databaseId) {
78
- return [method, route, String(statusCode), appId, databaseId].join(FIELD_SEP)
78
+ function buildFieldKey(method, route, statusCode, appId, databaseId, pid) {
79
+ return [method, route, String(statusCode), appId, databaseId, String(pid)].join(
80
+ FIELD_SEP
81
+ )
79
82
  }
80
83
 
81
84
  function hgetallPairsToObject(pairs) {
@@ -95,7 +98,7 @@ function hgetallPairsToObject(pairs) {
95
98
  *
96
99
  * **Structure:** `countKey` / `durKey` are each **one Redis hash**. The hash has **many fields** — not
97
100
  * 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`
101
+ * appId, databaseId, pid)`. The **value** in `:count` is **HINCRBY 1** per request for that group; in `:dur`
99
102
  * it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
100
103
  * Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
101
104
  */
@@ -142,7 +145,14 @@ class HttpMetricsRedisStore {
142
145
  record(method, route, statusCode, appId, databaseId, durationMs) {
143
146
  try {
144
147
  const client = this._ensureClient()
145
- const field = buildFieldKey(method, route, statusCode, appId, databaseId)
148
+ const field = buildFieldKey(
149
+ method,
150
+ route,
151
+ statusCode,
152
+ appId,
153
+ databaseId,
154
+ process.pid
155
+ )
146
156
  const dur = Math.max(0, Math.round(Number(durationMs) || 0))
147
157
  httpMetricsTraceLog(
148
158
  `save_to_redis pid=${process.pid} countKey=${this.countKey} ` +
@@ -220,13 +230,25 @@ class HttpMetricsRedisStore {
220
230
  }
221
231
  const dur = parseInt(durs[field] || '0', 10) || 0
222
232
  const parts = field.split(FIELD_SEP)
223
- if (parts.length !== 5) {
233
+ let m
234
+ let route
235
+ let statusStr
236
+ let aid
237
+ let did
238
+ let pidStr
239
+ if (parts.length === 6) {
240
+ ;[m, route, statusStr, aid, did, pidStr] = parts
241
+ } else if (parts.length === 5) {
242
+ ;[m, route, statusStr, aid, did] = parts
243
+ pidStr = 'legacy'
244
+ } else {
224
245
  continue
225
246
  }
226
- const [m, route, statusStr, aid, did] = parts
227
247
  sumRequests += count
228
248
  if (samples.length < 3) {
229
- samples.push(`${m} ${route} ${statusStr} x${count}`)
249
+ samples.push(
250
+ `${m} ${route} ${statusStr} pid=${pidStr} x${count}`
251
+ )
230
252
  }
231
253
  const labels = {
232
254
  method: m,
@@ -234,6 +256,7 @@ class HttpMetricsRedisStore {
234
256
  status_code: statusStr,
235
257
  appId: aid,
236
258
  databaseId: did,
259
+ pid: pidStr,
237
260
  }
238
261
  applyCount(labels, count)
239
262
  if (dur > 0) {