@adalo/metrics 0.0.0-staging.26 → 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.
- package/__tests__/httpMetricsRedisCollector.test.js +1 -1
- package/__tests__/httpMetricsRedisRecorder.test.js +1 -1
- package/__tests__/httpMetricsRedisStore.test.js +40 -7
- package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisCollector.js +2 -2
- package/lib/metrics/httpMetricsRedisCollector.js.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.d.ts +6 -4
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.js +25 -15
- package/lib/metrics/httpMetricsRedisStore.js.map +1 -1
- package/package.json +1 -1
- package/src/metrics/httpMetricsRedisCollector.js +2 -0
- package/src/metrics/httpMetricsRedisStore.js +33 -14
|
@@ -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;;;;;;;;;;;;;;
|
|
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,
|
|
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}
|
|
@@ -71,8 +72,9 @@ export function isRedisPeerInstalled(): boolean;
|
|
|
71
72
|
*/
|
|
72
73
|
export const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC: number;
|
|
73
74
|
/**
|
|
74
|
-
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
75
|
-
*
|
|
75
|
+
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
76
|
+
* Single `console.log` only: many hosts (e.g. Heroku) merge stdout+stderr into one stream and would
|
|
77
|
+
* show duplicate lines if we also wrote the same text to stderr.
|
|
76
78
|
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
77
79
|
*/
|
|
78
80
|
export function httpMetricsTraceLog(body: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"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"}
|
|
@@ -35,18 +35,14 @@ function truncField(s, max = 96) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
39
|
-
*
|
|
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.
|
|
40
41
|
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
41
42
|
*/
|
|
42
43
|
function httpMetricsTraceLog(body) {
|
|
43
44
|
const line = `${LOG} ${body}${clusterHint()}`;
|
|
44
45
|
console.log(line);
|
|
45
|
-
try {
|
|
46
|
-
process.stderr.write(`${line}\n`);
|
|
47
|
-
} catch (_) {
|
|
48
|
-
/* ignore */
|
|
49
|
-
}
|
|
50
46
|
}
|
|
51
47
|
const DRAIN_LUA = `
|
|
52
48
|
local function drain(key)
|
|
@@ -75,10 +71,11 @@ function isRedisPeerInstalled() {
|
|
|
75
71
|
* @param {number} statusCode
|
|
76
72
|
* @param {string} appId
|
|
77
73
|
* @param {string} databaseId
|
|
74
|
+
* @param {string} pid - OS process id (string); distinguishes cluster workers / processes in one Redis hash
|
|
78
75
|
* @returns {string}
|
|
79
76
|
*/
|
|
80
|
-
function buildFieldKey(method, route, statusCode, appId, databaseId) {
|
|
81
|
-
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);
|
|
82
79
|
}
|
|
83
80
|
function hgetallPairsToObject(pairs) {
|
|
84
81
|
const o = {};
|
|
@@ -97,7 +94,7 @@ function hgetallPairsToObject(pairs) {
|
|
|
97
94
|
*
|
|
98
95
|
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. The hash has **many fields** — not
|
|
99
96
|
* one bucket for all traffic. Each **field name** encodes one label group `(method, route, status_code,
|
|
100
|
-
* 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`
|
|
101
98
|
* it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
|
|
102
99
|
* Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
|
|
103
100
|
*/
|
|
@@ -144,7 +141,7 @@ class HttpMetricsRedisStore {
|
|
|
144
141
|
record(method, route, statusCode, appId, databaseId, durationMs) {
|
|
145
142
|
try {
|
|
146
143
|
const client = this._ensureClient();
|
|
147
|
-
const field = buildFieldKey(method, route, statusCode, appId, databaseId);
|
|
144
|
+
const field = buildFieldKey(method, route, statusCode, appId, databaseId, process.pid);
|
|
148
145
|
const dur = Math.max(0, Math.round(Number(durationMs) || 0));
|
|
149
146
|
httpMetricsTraceLog(`save_to_redis pid=${process.pid} countKey=${this.countKey} ` + `durKey=${this.durKey} field=${truncField(field)} durationMs=${dur} (before MULTI/EXEC)`);
|
|
150
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 => {
|
|
@@ -198,20 +195,33 @@ class HttpMetricsRedisStore {
|
|
|
198
195
|
}
|
|
199
196
|
const dur = parseInt(durs[field] || '0', 10) || 0;
|
|
200
197
|
const parts = field.split(FIELD_SEP);
|
|
201
|
-
|
|
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 {
|
|
202
212
|
continue;
|
|
203
213
|
}
|
|
204
|
-
const [m, route, statusStr, aid, did] = parts;
|
|
205
214
|
sumRequests += count;
|
|
206
215
|
if (samples.length < 3) {
|
|
207
|
-
samples.push(`${m} ${route} ${statusStr} x${count}`);
|
|
216
|
+
samples.push(`${m} ${route} ${statusStr} pid=${pidStr} x${count}`);
|
|
208
217
|
}
|
|
209
218
|
const labels = {
|
|
210
219
|
method: m,
|
|
211
220
|
route,
|
|
212
221
|
status_code: statusStr,
|
|
213
222
|
appId: aid,
|
|
214
|
-
databaseId: did
|
|
223
|
+
databaseId: did,
|
|
224
|
+
pid: pidStr
|
|
215
225
|
};
|
|
216
226
|
applyCount(labels, count);
|
|
217
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","process","stderr","write","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","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). Emits **stdout + stderr**\n * so hosts that treat streams differently still show something.\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 try {\n process.stderr.write(`${line}\\n`)\n } catch (_) {\n /* ignore */\n }\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
|
@@ -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
|
})
|
|
@@ -35,18 +35,14 @@ function truncField(s, max = 96) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Trace line for SAVE (web → Redis) and GET/drain (collector ← Redis).
|
|
39
|
-
*
|
|
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.
|
|
40
41
|
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
41
42
|
*/
|
|
42
43
|
function httpMetricsTraceLog(body) {
|
|
43
44
|
const line = `${LOG} ${body}${clusterHint()}`
|
|
44
45
|
console.log(line)
|
|
45
|
-
try {
|
|
46
|
-
process.stderr.write(`${line}\n`)
|
|
47
|
-
} catch (_) {
|
|
48
|
-
/* ignore */
|
|
49
|
-
}
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
const DRAIN_LUA = `
|
|
@@ -76,10 +72,13 @@ function isRedisPeerInstalled() {
|
|
|
76
72
|
* @param {number} statusCode
|
|
77
73
|
* @param {string} appId
|
|
78
74
|
* @param {string} databaseId
|
|
75
|
+
* @param {string} pid - OS process id (string); distinguishes cluster workers / processes in one Redis hash
|
|
79
76
|
* @returns {string}
|
|
80
77
|
*/
|
|
81
|
-
function buildFieldKey(method, route, statusCode, appId, databaseId) {
|
|
82
|
-
return [method, route, String(statusCode), appId, databaseId].join(
|
|
78
|
+
function buildFieldKey(method, route, statusCode, appId, databaseId, pid) {
|
|
79
|
+
return [method, route, String(statusCode), appId, databaseId, String(pid)].join(
|
|
80
|
+
FIELD_SEP
|
|
81
|
+
)
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
function hgetallPairsToObject(pairs) {
|
|
@@ -99,7 +98,7 @@ function hgetallPairsToObject(pairs) {
|
|
|
99
98
|
*
|
|
100
99
|
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. The hash has **many fields** — not
|
|
101
100
|
* one bucket for all traffic. Each **field name** encodes one label group `(method, route, status_code,
|
|
102
|
-
* 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`
|
|
103
102
|
* it is **HINCRBY** of that request’s **duration ms** (so many requests → summed ms per same field).
|
|
104
103
|
* Different routes → different hash fields. Drain runs atomic Lua (HGETALL + DEL) per tick.
|
|
105
104
|
*/
|
|
@@ -146,7 +145,14 @@ class HttpMetricsRedisStore {
|
|
|
146
145
|
record(method, route, statusCode, appId, databaseId, durationMs) {
|
|
147
146
|
try {
|
|
148
147
|
const client = this._ensureClient()
|
|
149
|
-
const field = buildFieldKey(
|
|
148
|
+
const field = buildFieldKey(
|
|
149
|
+
method,
|
|
150
|
+
route,
|
|
151
|
+
statusCode,
|
|
152
|
+
appId,
|
|
153
|
+
databaseId,
|
|
154
|
+
process.pid
|
|
155
|
+
)
|
|
150
156
|
const dur = Math.max(0, Math.round(Number(durationMs) || 0))
|
|
151
157
|
httpMetricsTraceLog(
|
|
152
158
|
`save_to_redis pid=${process.pid} countKey=${this.countKey} ` +
|
|
@@ -224,13 +230,25 @@ class HttpMetricsRedisStore {
|
|
|
224
230
|
}
|
|
225
231
|
const dur = parseInt(durs[field] || '0', 10) || 0
|
|
226
232
|
const parts = field.split(FIELD_SEP)
|
|
227
|
-
|
|
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 {
|
|
228
245
|
continue
|
|
229
246
|
}
|
|
230
|
-
const [m, route, statusStr, aid, did] = parts
|
|
231
247
|
sumRequests += count
|
|
232
248
|
if (samples.length < 3) {
|
|
233
|
-
samples.push(
|
|
249
|
+
samples.push(
|
|
250
|
+
`${m} ${route} ${statusStr} pid=${pidStr} x${count}`
|
|
251
|
+
)
|
|
234
252
|
}
|
|
235
253
|
const labels = {
|
|
236
254
|
method: m,
|
|
@@ -238,6 +256,7 @@ class HttpMetricsRedisStore {
|
|
|
238
256
|
status_code: statusStr,
|
|
239
257
|
appId: aid,
|
|
240
258
|
databaseId: did,
|
|
259
|
+
pid: pidStr,
|
|
241
260
|
}
|
|
242
261
|
applyCount(labels, count)
|
|
243
262
|
if (dur > 0) {
|