@adalo/metrics 0.0.0-staging.22 → 0.0.0-staging.24
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__/httpMetricsRedisStore.test.js +51 -0
- package/lib/metrics/httpMetricsRedisCollector.d.ts +4 -3
- package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisCollector.js +5 -4
- package/lib/metrics/httpMetricsRedisCollector.js.map +1 -1
- package/lib/metrics/httpMetricsRedisRecorder.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisRecorder.js +7 -1
- package/lib/metrics/httpMetricsRedisRecorder.js.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.d.ts +8 -1
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.js +42 -2
- package/lib/metrics/httpMetricsRedisStore.js.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.d.ts +2 -5
- package/lib/metrics/metricsProcessTypeUtils.d.ts.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.js +2 -7
- package/lib/metrics/metricsProcessTypeUtils.js.map +1 -1
- package/package.json +5 -5
- package/src/metrics/httpMetricsRedisCollector.js +5 -7
- package/src/metrics/httpMetricsRedisRecorder.js +11 -1
- package/src/metrics/httpMetricsRedisStore.js +54 -1
- package/src/metrics/metricsProcessTypeUtils.js +2 -7
|
@@ -97,6 +97,57 @@ describe('HttpMetricsRedisStore', () => {
|
|
|
97
97
|
expect(redis.eval).not.toHaveBeenCalled()
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
+
it('applies aggregated count and summed duration for one route (many requests → one field)', async () => {
|
|
101
|
+
const redis = createRedisV3Mock()
|
|
102
|
+
redis.set.mockImplementation((key, val, mode, ttl, nx, cb) => {
|
|
103
|
+
cb(null, 'OK')
|
|
104
|
+
})
|
|
105
|
+
const field = buildFieldKey('GET', '/api/items', 200, '', '')
|
|
106
|
+
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
107
|
+
cb(null, [
|
|
108
|
+
[field, '100'],
|
|
109
|
+
[field, '4500'],
|
|
110
|
+
])
|
|
111
|
+
})
|
|
112
|
+
redis.del.mockImplementation((key, cb) => {
|
|
113
|
+
if (cb) {
|
|
114
|
+
cb()
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const store = new HttpMetricsRedisStore({
|
|
119
|
+
redisClient: redis,
|
|
120
|
+
appName: 'app',
|
|
121
|
+
processType: 'web',
|
|
122
|
+
})
|
|
123
|
+
const applyCount = jest.fn()
|
|
124
|
+
const applyDuration = jest.fn()
|
|
125
|
+
|
|
126
|
+
const ok = await store.flushToCounters(applyCount, applyDuration)
|
|
127
|
+
|
|
128
|
+
expect(ok).toBe(true)
|
|
129
|
+
expect(applyCount).toHaveBeenCalledWith(
|
|
130
|
+
{
|
|
131
|
+
method: 'GET',
|
|
132
|
+
route: '/api/items',
|
|
133
|
+
status_code: '200',
|
|
134
|
+
appId: '',
|
|
135
|
+
databaseId: '',
|
|
136
|
+
},
|
|
137
|
+
100
|
|
138
|
+
)
|
|
139
|
+
expect(applyDuration).toHaveBeenCalledWith(
|
|
140
|
+
{
|
|
141
|
+
method: 'GET',
|
|
142
|
+
route: '/api/items',
|
|
143
|
+
status_code: '200',
|
|
144
|
+
appId: '',
|
|
145
|
+
databaseId: '',
|
|
146
|
+
},
|
|
147
|
+
4500
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
100
151
|
it('drains hashes and applies count and duration', async () => {
|
|
101
152
|
const redis = createRedisV3Mock()
|
|
102
153
|
redis.set.mockImplementation((key, val, mode, ttl, nx, cb) => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),
|
|
3
3
|
* applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.
|
|
5
|
+
* `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.
|
|
6
|
+
* Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).
|
|
6
7
|
*
|
|
7
8
|
* @extends BaseMetricsClient
|
|
8
9
|
*/
|
|
@@ -21,7 +22,7 @@ export class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
21
22
|
* @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported
|
|
22
23
|
* @param {function} [config.startupValidation] Run before first push
|
|
23
24
|
* @param {boolean} [config.disablePushgateway] Skip POST to VM-agent
|
|
24
|
-
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default
|
|
25
|
+
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.
|
|
25
26
|
* @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)
|
|
26
27
|
*/
|
|
27
28
|
constructor(config?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisCollector.js"],"names":[],"mappings":"AAGA
|
|
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"}
|
|
@@ -10,8 +10,9 @@ const {
|
|
|
10
10
|
/**
|
|
11
11
|
* Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),
|
|
12
12
|
* applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.
|
|
14
|
+
* `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.
|
|
15
|
+
* Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).
|
|
15
16
|
*
|
|
16
17
|
* @extends BaseMetricsClient
|
|
17
18
|
*/
|
|
@@ -30,7 +31,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
30
31
|
* @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported
|
|
31
32
|
* @param {function} [config.startupValidation] Run before first push
|
|
32
33
|
* @param {boolean} [config.disablePushgateway] Skip POST to VM-agent
|
|
33
|
-
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default
|
|
34
|
+
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.
|
|
34
35
|
* @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)
|
|
35
36
|
*/
|
|
36
37
|
constructor(config = {}) {
|
|
@@ -44,7 +45,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
44
45
|
...config,
|
|
45
46
|
blockNodeDefaultMetrics: true
|
|
46
47
|
});
|
|
47
|
-
const keyProcessType = config.redisProcessTypeForKeys ||
|
|
48
|
+
const keyProcessType = config.redisProcessTypeForKeys || 'web';
|
|
48
49
|
this.defaultLabelsWithoutDynoId = {
|
|
49
50
|
app: this.appName,
|
|
50
51
|
process_type: keyProcessType
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisRecorder.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACd,OAAO,EAApB,MAAM;QACO,WAAW,EAAxB,MAAM;QACQ,MAAM;OAc9B;IARC,oBAA8B;IAC9B,gBAAsB;IACtB,8BAKE;IAGJ;;;;;;;;OAQG;IACH;QAP0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACU,KAAK;QACL,UAAU;QACX,QAAQ,EAAvB,MAAM;aAgBhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAkCvC;CACF"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
HttpMetricsRedisStore
|
|
4
|
+
HttpMetricsRedisStore,
|
|
5
|
+
httpMetricsTraceLog
|
|
5
6
|
} = require('./httpMetricsRedisStore');
|
|
7
|
+
function trunc(s, max = 120) {
|
|
8
|
+
const t = String(s);
|
|
9
|
+
return t.length > max ? `${t.slice(0, max - 3)}...` : t;
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
@@ -54,6 +59,7 @@ class HttpMetricsRedisRecorder {
|
|
|
54
59
|
databaseId = '',
|
|
55
60
|
duration
|
|
56
61
|
}) {
|
|
62
|
+
httpMetricsTraceLog(`1_track pid=${process.pid} app=${this.appName} segment=${this.processType} ` + `method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` + `appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)}`);
|
|
57
63
|
this._store.record(method, route, status_code, appId, databaseId, duration);
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisRecorder.js","names":["HttpMetricsRedisStore","require","HttpMetricsRedisRecorder","constructor","redisClient","appName","processType","ttlSec","Error","_store","trackHttpRequest","method","route","status_code","appId","databaseId","duration","record","trackHttpRequestMiddleware","req","res","next","start","Date","now","on","path","params","body","query","datasourceId","statusCode","module","exports"],"sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"sourcesContent":["const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\n/**\n * Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).\n * Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.\n *\n * @see HttpMetricsRedisStore\n */\nclass HttpMetricsRedisRecorder {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} opts.appName Application name (must match collector; typically `BUILD_APP_NAME`).\n * @param {string} opts.processType Logical process segment in Redis keys (e.g. `web`; must match collector’s key segment).\n * @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisRecorder: redisClient is required')\n }\n this.processType = processType\n this.appName = appName\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName,\n processType,\n ttlSec,\n })\n }\n\n /**\n * @param {Object} params\n * @param {string} params.method\n * @param {string} params.route\n * @param {number} params.status_code\n * @param {string} [params.appId]\n * @param {string} [params.databaseId]\n * @param {number} params.duration\n */\n trackHttpRequest({\n method,\n route,\n status_code,\n appId = '',\n databaseId = '',\n duration,\n }) {\n this._store.record(method, route, status_code, appId, databaseId, duration)\n }\n\n /**\n * Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.\n * Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.\n *\n * @param {import('http').IncomingMessage} req\n * @param {import('http').ServerResponse} res\n * @param {function} next\n */\n trackHttpRequestMiddleware = (req, res, next) => {\n if (req.method === 'OPTIONS') {\n next()\n return\n }\n\n const start = Date.now()\n res.on('finish', () => {\n const route = req.route?.path || req.path || 'unknown'\n const appId =\n req.params?.appId || req.body?.appId || req.query?.appId || ''\n const databaseId =\n req.params?.databaseId ||\n req.body?.databaseId ||\n req.query?.databaseId ||\n req.params?.datasourceId ||\n req.body?.datasourceId ||\n req.query?.datasourceId ||\n ''\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n appId,\n databaseId,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { HttpMetricsRedisRecorder }\n"],"mappings":";;AAAA,MAAM;EAAEA;
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.js","names":["HttpMetricsRedisStore","httpMetricsTraceLog","require","trunc","s","max","t","String","length","slice","HttpMetricsRedisRecorder","constructor","redisClient","appName","processType","ttlSec","Error","_store","trackHttpRequest","method","route","status_code","appId","databaseId","duration","process","pid","record","trackHttpRequestMiddleware","req","res","next","start","Date","now","on","path","params","body","query","datasourceId","statusCode","module","exports"],"sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"sourcesContent":["const { HttpMetricsRedisStore, httpMetricsTraceLog } = require('./httpMetricsRedisStore')\n\nfunction trunc(s, max = 120) {\n const t = String(s)\n return t.length > max ? `${t.slice(0, max - 3)}...` : t\n}\n\n/**\n * Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).\n * Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.\n *\n * @see HttpMetricsRedisStore\n */\nclass HttpMetricsRedisRecorder {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} opts.appName Application name (must match collector; typically `BUILD_APP_NAME`).\n * @param {string} opts.processType Logical process segment in Redis keys (e.g. `web`; must match collector’s key segment).\n * @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisRecorder: redisClient is required')\n }\n this.processType = processType\n this.appName = appName\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName,\n processType,\n ttlSec,\n })\n }\n\n /**\n * @param {Object} params\n * @param {string} params.method\n * @param {string} params.route\n * @param {number} params.status_code\n * @param {string} [params.appId]\n * @param {string} [params.databaseId]\n * @param {number} params.duration\n */\n trackHttpRequest({\n method,\n route,\n status_code,\n appId = '',\n databaseId = '',\n duration,\n }) {\n httpMetricsTraceLog(\n `1_track pid=${process.pid} app=${this.appName} segment=${this.processType} ` +\n `method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` +\n `appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)}`\n )\n this._store.record(method, route, status_code, appId, databaseId, duration)\n }\n\n /**\n * Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.\n * Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.\n *\n * @param {import('http').IncomingMessage} req\n * @param {import('http').ServerResponse} res\n * @param {function} next\n */\n trackHttpRequestMiddleware = (req, res, next) => {\n if (req.method === 'OPTIONS') {\n next()\n return\n }\n\n const start = Date.now()\n res.on('finish', () => {\n const route = req.route?.path || req.path || 'unknown'\n const appId =\n req.params?.appId || req.body?.appId || req.query?.appId || ''\n const databaseId =\n req.params?.databaseId ||\n req.body?.databaseId ||\n req.query?.databaseId ||\n req.params?.datasourceId ||\n req.body?.datasourceId ||\n req.query?.datasourceId ||\n ''\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n appId,\n databaseId,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { HttpMetricsRedisRecorder }\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.
|
|
4
4
|
*
|
|
5
5
|
* **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`
|
|
6
|
-
* (sum of duration ms per same field).
|
|
6
|
+
* (sum of duration ms per same field). Same `(method, route, status, appId, databaseId)` → same hash field;
|
|
7
|
+
* many requests in an interval **add** to count and **sum** durations (Redis `HINCRBY`). Drain applies those
|
|
8
|
+
* totals to `app_requests_total` / `app_requests_total_duration`.
|
|
7
9
|
*/
|
|
8
10
|
export class HttpMetricsRedisStore {
|
|
9
11
|
/**
|
|
@@ -68,4 +70,9 @@ export function isRedisPeerInstalled(): boolean;
|
|
|
68
70
|
* @type {number}
|
|
69
71
|
*/
|
|
70
72
|
export const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC: number;
|
|
73
|
+
/**
|
|
74
|
+
* Stdout trace (uses **console.log**, not warn — same as typical app request logs).
|
|
75
|
+
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
76
|
+
*/
|
|
77
|
+
export function httpMetricsTraceLog(body: string): void;
|
|
71
78
|
//# sourceMappingURL=httpMetricsRedisStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"AAmFA;;;;;;;;GAQG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACd,OAAO,EAApB,MAAM;QACO,WAAW,EAAxB,MAAM;QACQ,MAAM;OAiB9B;IAXC,qCAA0B;IAC1B,eAGwC;IAIxC,iBAAiD;IACjD,eAA6C;IAC7C,gBAAqD;IAGvD;;;OAGG;IACH,sBAEC;IAED;;;;;;;OAOG;IACH,eAPW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,cACN,MAAM,QAyBhB;IAED;;;;OAIG;IACH,qCAJoB,MAAM,SAAS,MAAM,KAAK,IAAI,0BAC9B,MAAM,SAAS,MAAM,KAAK,IAAI,GACrC,QAAQ,OAAO,CAAC,CAgG5B;CACF;AAtMD;;;;;;;GAOG;AACH,sCAPW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AAtED;;;GAGG;AACH,wBAFU,MAAM,CAEQ;AA4CxB;;GAEG;AACH,wCAFa,OAAO,CASnB;AApDD;;;GAGG;AACH,iDAFU,MAAM,CAE8B;AAqB9C;;;GAGG;AACH,0CAFW,MAAM,QAIhB"}
|
|
@@ -11,6 +11,32 @@ const FIELD_SEP = '\x1e';
|
|
|
11
11
|
* @type {number}
|
|
12
12
|
*/
|
|
13
13
|
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120;
|
|
14
|
+
const LOG = '[http-metrics-redis]';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* When Node runs behind `cluster`, HTTP is usually served from workers — include worker id in traces.
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function clusterHint() {
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line global-require
|
|
23
|
+
const c = require('cluster');
|
|
24
|
+
if (c.isWorker && c.worker != null) {
|
|
25
|
+
return ` cluster_worker=${c.worker.id}`;
|
|
26
|
+
}
|
|
27
|
+
} catch (_) {
|
|
28
|
+
/* cluster not in use */
|
|
29
|
+
}
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Stdout trace (uses **console.log**, not warn — same as typical app request logs).
|
|
35
|
+
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
36
|
+
*/
|
|
37
|
+
function httpMetricsTraceLog(body) {
|
|
38
|
+
console.log(`${LOG} ${body}${clusterHint()}`);
|
|
39
|
+
}
|
|
14
40
|
const DRAIN_LUA = `
|
|
15
41
|
local function drain(key)
|
|
16
42
|
local v = redis.call('HGETALL', key)
|
|
@@ -59,7 +85,9 @@ function hgetallPairsToObject(pairs) {
|
|
|
59
85
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.
|
|
60
86
|
*
|
|
61
87
|
* **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`
|
|
62
|
-
* (sum of duration ms per same field).
|
|
88
|
+
* (sum of duration ms per same field). Same `(method, route, status, appId, databaseId)` → same hash field;
|
|
89
|
+
* many requests in an interval **add** to count and **sum** durations (Redis `HINCRBY`). Drain applies those
|
|
90
|
+
* totals to `app_requests_total` / `app_requests_total_duration`.
|
|
63
91
|
*/
|
|
64
92
|
class HttpMetricsRedisStore {
|
|
65
93
|
/**
|
|
@@ -110,7 +138,9 @@ class HttpMetricsRedisStore {
|
|
|
110
138
|
client.multi().hincrby(this.countKey, field, 1).hincrby(this.durKey, field, dur).expire(this.countKey, this.ttlSec).expire(this.durKey, this.ttlSec).exec(err => {
|
|
111
139
|
if (err) {
|
|
112
140
|
console.error('[HttpMetricsRedisStore] record failed:', err.message);
|
|
141
|
+
return;
|
|
113
142
|
}
|
|
143
|
+
httpMetricsTraceLog(`2_redis_write pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} ok`);
|
|
114
144
|
});
|
|
115
145
|
} catch (e) {
|
|
116
146
|
console.error('[HttpMetricsRedisStore] record:', e.message);
|
|
@@ -133,6 +163,7 @@ class HttpMetricsRedisStore {
|
|
|
133
163
|
return new Promise(resolve => {
|
|
134
164
|
client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {
|
|
135
165
|
if (setErr || ok !== 'OK') {
|
|
166
|
+
httpMetricsTraceLog(`3_redis_drain_skip pid=${process.pid} countKey=${this.countKey} reason=${setErr ? `error:${setErr.message}` : 'lock_not_acquired'}`);
|
|
136
167
|
resolve(false);
|
|
137
168
|
return;
|
|
138
169
|
}
|
|
@@ -147,12 +178,15 @@ class HttpMetricsRedisStore {
|
|
|
147
178
|
}
|
|
148
179
|
try {
|
|
149
180
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
181
|
+
httpMetricsTraceLog(`3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (empty_after_lua)`);
|
|
150
182
|
finish();
|
|
151
183
|
return;
|
|
152
184
|
}
|
|
153
185
|
const counts = hgetallPairsToObject(raw[0]);
|
|
154
186
|
const durs = hgetallPairsToObject(raw[1]);
|
|
155
187
|
const fieldKeys = Object.keys(counts);
|
|
188
|
+
let sumRequests = 0;
|
|
189
|
+
const samples = [];
|
|
156
190
|
for (const field of fieldKeys) {
|
|
157
191
|
const count = parseInt(counts[field], 10);
|
|
158
192
|
if (!count || count < 1) {
|
|
@@ -164,6 +198,10 @@ class HttpMetricsRedisStore {
|
|
|
164
198
|
continue;
|
|
165
199
|
}
|
|
166
200
|
const [m, route, statusStr, aid, did] = parts;
|
|
201
|
+
sumRequests += count;
|
|
202
|
+
if (samples.length < 3) {
|
|
203
|
+
samples.push(`${m} ${route} ${statusStr} x${count}`);
|
|
204
|
+
}
|
|
167
205
|
const labels = {
|
|
168
206
|
method: m,
|
|
169
207
|
route,
|
|
@@ -176,6 +214,7 @@ class HttpMetricsRedisStore {
|
|
|
176
214
|
applyDuration(labels, dur);
|
|
177
215
|
}
|
|
178
216
|
}
|
|
217
|
+
httpMetricsTraceLog(`3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=${fieldKeys.length} sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`);
|
|
179
218
|
} catch (e) {
|
|
180
219
|
console.error('[HttpMetricsRedisStore] flush apply failed:', e.message);
|
|
181
220
|
}
|
|
@@ -190,6 +229,7 @@ module.exports = {
|
|
|
190
229
|
buildFieldKey,
|
|
191
230
|
FIELD_SEP,
|
|
192
231
|
isRedisPeerInstalled,
|
|
193
|
-
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC
|
|
232
|
+
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,
|
|
233
|
+
httpMetricsTraceLog
|
|
194
234
|
};
|
|
195
235
|
//# sourceMappingURL=httpMetricsRedisStore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","DRAIN_LUA","isRedisPeerInstalled","require","resolve","buildFieldKey","method","route","statusCode","appId","databaseId","String","join","hgetallPairsToObject","pairs","o","length","i","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","lockKey","_ensureClient","record","durationMs","client","field","dur","Math","max","round","Number","multi","hincrby","expire","exec","err","console","error","message","e","flushToCounters","applyCount","applyDuration","Promise","set","setErr","ok","eval","evalErr","raw","finish","del","Array","isArray","counts","durs","fieldKeys","Object","keys","count","parseInt","parts","split","m","statusStr","aid","did","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 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`, `set`, `del`.\n *\n * **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`\n * (sum of duration ms per same field). Hash field = `method\\\\x1eroute\\\\x1estatus\\\\x1eappId\\\\x1edatabaseId`.\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 this.lockKey = `metrics:http:v2:${keySeg}:drain_lock`\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 client\n .multi()\n .hincrby(this.countKey, field, 1)\n .hincrby(this.durKey, field, dur)\n .expire(this.countKey, this.ttlSec)\n .expire(this.durKey, this.ttlSec)\n .exec(err => {\n if (err) {\n console.error('[HttpMetricsRedisStore] record failed:', err.message)\n }\n })\n } catch (e) {\n console.error('[HttpMetricsRedisStore] record:', e.message)\n }\n }\n\n /**\n * @param {(labels: Object, value: number) => void} applyCount\n * @param {(labels: Object, value: number) => void} applyDuration\n * @returns {Promise<boolean>}\n */\n flushToCounters(applyCount, applyDuration) {\n let client\n try {\n client = this._ensureClient()\n } catch (e) {\n console.error('[HttpMetricsRedisStore] flush:', e.message)\n return Promise.resolve(false)\n }\n return new Promise(resolve => {\n client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {\n if (setErr || ok !== 'OK') {\n resolve(false)\n return\n }\n client.eval(\n DRAIN_LUA,\n 2,\n this.countKey,\n this.durKey,\n (evalErr, raw) => {\n const finish = () => {\n client.del(this.lockKey, () => resolve(true))\n }\n\n if (evalErr) {\n console.error(\n '[HttpMetricsRedisStore] drain failed:',\n evalErr.message\n )\n finish()\n return\n }\n\n try {\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n finish()\n return\n }\n const counts = hgetallPairsToObject(raw[0])\n const durs = hgetallPairsToObject(raw[1])\n const fieldKeys = Object.keys(counts)\n for (const field of fieldKeys) {\n const count = parseInt(counts[field], 10)\n if (!count || count < 1) {\n continue\n }\n const dur = parseInt(durs[field] || '0', 10) || 0\n const parts = field.split(FIELD_SEP)\n if (parts.length !== 5) {\n continue\n }\n const [m, route, statusStr, aid, did] = parts\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 } catch (e) {\n console.error(\n '[HttpMetricsRedisStore] flush apply failed:',\n e.message\n )\n }\n finish()\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}\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA,MAAMA,SAAS,GAAG,MAAM;;AAExB;AACA;AACA;AACA;AACA,MAAMC,kCAAkC,GAAG,GAAG;AAE9C,MAAMC,SAAS,GAAG;AAClB;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED;AACA;AACA;AACA,SAASC,oBAAoBA,CAAA,EAAG;EAC9B,IAAI;IACFC,OAAO,CAACC,OAAO,CAAC,OAAO,CAAC;IACxB,OAAO,IAAI;EACb,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,aAAaA,CAACC,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,EAAE;EACnE,OAAO,CAACJ,MAAM,EAAEC,KAAK,EAAEI,MAAM,CAACH,UAAU,CAAC,EAAEC,KAAK,EAAEC,UAAU,CAAC,CAACE,IAAI,CAACb,SAAS,CAAC;AAC/E;AAEA,SAASc,oBAAoBA,CAACC,KAAK,EAAE;EACnC,MAAMC,CAAC,GAAG,CAAC,CAAC;EACZ,IAAI,CAACD,KAAK,IAAI,CAACA,KAAK,CAACE,MAAM,EAAE;IAC3B,OAAOD,CAAC;EACV;EACA,KAAK,IAAIE,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGH,KAAK,CAACE,MAAM,EAAEC,CAAC,IAAI,CAAC,EAAE;IACxCF,CAAC,CAACD,KAAK,CAACG,CAAC,CAAC,CAAC,GAAGH,KAAK,CAACG,CAAC,GAAG,CAAC,CAAC;EAC5B;EACA,OAAOF,CAAC;AACV;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMG,qBAAqB,CAAC;EAC1B;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAAC;IAAEC,WAAW;IAAEC,OAAO;IAAEC,WAAW;IAAEC;EAAO,CAAC,EAAE;IACzD,IAAIH,WAAW,IAAI,IAAI,EAAE;MACvB,MAAM,IAAII,KAAK,CAAC,gDAAgD,CAAC;IACnE;IACA,IAAI,CAACC,OAAO,GAAGL,WAAW;IAC1B,IAAI,CAACG,MAAM,GACT,OAAOA,MAAM,KAAK,QAAQ,IAAIA,MAAM,GAAG,CAAC,GACpCA,MAAM,GACNvB,kCAAkC;IACxC,MAAM0B,MAAM,GAAG,GAAGC,kBAAkB,CAACN,OAAO,CAAC,IAAIM,kBAAkB,CACjEL,WACF,CAAC,EAAE;IACH,IAAI,CAACM,QAAQ,GAAG,mBAAmBF,MAAM,QAAQ;IACjD,IAAI,CAACG,MAAM,GAAG,mBAAmBH,MAAM,MAAM;IAC7C,IAAI,CAACI,OAAO,GAAG,mBAAmBJ,MAAM,aAAa;EACvD;;EAEA;AACF;AACA;AACA;EACEK,aAAaA,CAAA,EAAG;IACd,OAAO,IAAI,CAACN,OAAO;EACrB;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACEO,MAAMA,CAAC1B,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,EAAEuB,UAAU,EAAE;IAC/D,IAAI;MACF,MAAMC,MAAM,GAAG,IAAI,CAACH,aAAa,CAAC,CAAC;MACnC,MAAMI,KAAK,GAAG9B,aAAa,CAACC,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,CAAC;MACzE,MAAM0B,GAAG,GAAGC,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,KAAK,CAACC,MAAM,CAACP,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;MAC5DC,MAAM,CACHO,KAAK,CAAC,CAAC,CACPC,OAAO,CAAC,IAAI,CAACd,QAAQ,EAAEO,KAAK,EAAE,CAAC,CAAC,CAChCO,OAAO,CAAC,IAAI,CAACb,MAAM,EAAEM,KAAK,EAAEC,GAAG,CAAC,CAChCO,MAAM,CAAC,IAAI,CAACf,QAAQ,EAAE,IAAI,CAACL,MAAM,CAAC,CAClCoB,MAAM,CAAC,IAAI,CAACd,MAAM,EAAE,IAAI,CAACN,MAAM,CAAC,CAChCqB,IAAI,CAACC,GAAG,IAAI;QACX,IAAIA,GAAG,EAAE;UACPC,OAAO,CAACC,KAAK,CAAC,wCAAwC,EAAEF,GAAG,CAACG,OAAO,CAAC;QACtE;MACF,CAAC,CAAC;IACN,CAAC,CAAC,OAAOC,CAAC,EAAE;MACVH,OAAO,CAACC,KAAK,CAAC,iCAAiC,EAAEE,CAAC,CAACD,OAAO,CAAC;IAC7D;EACF;;EAEA;AACF;AACA;AACA;AACA;EACEE,eAAeA,CAACC,UAAU,EAAEC,aAAa,EAAE;IACzC,IAAIlB,MAAM;IACV,IAAI;MACFA,MAAM,GAAG,IAAI,CAACH,aAAa,CAAC,CAAC;IAC/B,CAAC,CAAC,OAAOkB,CAAC,EAAE;MACVH,OAAO,CAACC,KAAK,CAAC,gCAAgC,EAAEE,CAAC,CAACD,OAAO,CAAC;MAC1D,OAAOK,OAAO,CAACjD,OAAO,CAAC,KAAK,CAAC;IAC/B;IACA,OAAO,IAAIiD,OAAO,CAACjD,OAAO,IAAI;MAC5B8B,MAAM,CAACoB,GAAG,CAAC,IAAI,CAACxB,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAACyB,MAAM,EAAEC,EAAE,KAAK;QAC5D,IAAID,MAAM,IAAIC,EAAE,KAAK,IAAI,EAAE;UACzBpD,OAAO,CAAC,KAAK,CAAC;UACd;QACF;QACA8B,MAAM,CAACuB,IAAI,CACTxD,SAAS,EACT,CAAC,EACD,IAAI,CAAC2B,QAAQ,EACb,IAAI,CAACC,MAAM,EACX,CAAC6B,OAAO,EAAEC,GAAG,KAAK;UAChB,MAAMC,MAAM,GAAGA,CAAA,KAAM;YACnB1B,MAAM,CAAC2B,GAAG,CAAC,IAAI,CAAC/B,OAAO,EAAE,MAAM1B,OAAO,CAAC,IAAI,CAAC,CAAC;UAC/C,CAAC;UAED,IAAIsD,OAAO,EAAE;YACXZ,OAAO,CAACC,KAAK,CACX,uCAAuC,EACvCW,OAAO,CAACV,OACV,CAAC;YACDY,MAAM,CAAC,CAAC;YACR;UACF;UAEA,IAAI;YACF,IAAI,CAACD,GAAG,IAAI,CAACG,KAAK,CAACC,OAAO,CAACJ,GAAG,CAAC,IAAIA,GAAG,CAAC3C,MAAM,GAAG,CAAC,EAAE;cACjD4C,MAAM,CAAC,CAAC;cACR;YACF;YACA,MAAMI,MAAM,GAAGnD,oBAAoB,CAAC8C,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAMM,IAAI,GAAGpD,oBAAoB,CAAC8C,GAAG,CAAC,CAAC,CAAC,CAAC;YACzC,MAAMO,SAAS,GAAGC,MAAM,CAACC,IAAI,CAACJ,MAAM,CAAC;YACrC,KAAK,MAAM7B,KAAK,IAAI+B,SAAS,EAAE;cAC7B,MAAMG,KAAK,GAAGC,QAAQ,CAACN,MAAM,CAAC7B,KAAK,CAAC,EAAE,EAAE,CAAC;cACzC,IAAI,CAACkC,KAAK,IAAIA,KAAK,GAAG,CAAC,EAAE;gBACvB;cACF;cACA,MAAMjC,GAAG,GAAGkC,QAAQ,CAACL,IAAI,CAAC9B,KAAK,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC;cACjD,MAAMoC,KAAK,GAAGpC,KAAK,CAACqC,KAAK,CAACzE,SAAS,CAAC;cACpC,IAAIwE,KAAK,CAACvD,MAAM,KAAK,CAAC,EAAE;gBACtB;cACF;cACA,MAAM,CAACyD,CAAC,EAAElE,KAAK,EAAEmE,SAAS,EAAEC,GAAG,EAAEC,GAAG,CAAC,GAAGL,KAAK;cAC7C,MAAMM,MAAM,GAAG;gBACbvE,MAAM,EAAEmE,CAAC;gBACTlE,KAAK;gBACLuE,WAAW,EAAEJ,SAAS;gBACtBjE,KAAK,EAAEkE,GAAG;gBACVjE,UAAU,EAAEkE;cACd,CAAC;cACDzB,UAAU,CAAC0B,MAAM,EAAER,KAAK,CAAC;cACzB,IAAIjC,GAAG,GAAG,CAAC,EAAE;gBACXgB,aAAa,CAACyB,MAAM,EAAEzC,GAAG,CAAC;cAC5B;YACF;UACF,CAAC,CAAC,OAAOa,CAAC,EAAE;YACVH,OAAO,CAACC,KAAK,CACX,6CAA6C,EAC7CE,CAAC,CAACD,OACJ,CAAC;UACH;UACAY,MAAM,CAAC,CAAC;QACV,CACF,CAAC;MACH,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ;AACF;AAEAmB,MAAM,CAACC,OAAO,GAAG;EACf9D,qBAAqB;EACrBb,aAAa;EACbN,SAAS;EACTG,oBAAoB;EACpBF;AACF,CAAC","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","LOG","clusterHint","c","require","isWorker","worker","id","_","httpMetricsTraceLog","body","console","log","DRAIN_LUA","isRedisPeerInstalled","resolve","buildFieldKey","method","route","statusCode","appId","databaseId","String","join","hgetallPairsToObject","pairs","o","length","i","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","lockKey","_ensureClient","record","durationMs","client","field","dur","Math","max","round","Number","multi","hincrby","expire","exec","err","error","message","process","pid","e","flushToCounters","applyCount","applyDuration","Promise","set","setErr","ok","eval","evalErr","raw","finish","del","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\n/**\n * Stdout trace (uses **console.log**, not warn — same as typical app request logs).\n * @param {string} body - Line body after `[http-metrics-redis]` prefix.\n */\nfunction httpMetricsTraceLog(body) {\n console.log(`${LOG} ${body}${clusterHint()}`)\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`, `set`, `del`.\n *\n * **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`\n * (sum of duration ms per same field). Same `(method, route, status, appId, databaseId)` → same hash field;\n * many requests in an interval **add** to count and **sum** durations (Redis `HINCRBY`). Drain applies those\n * totals to `app_requests_total` / `app_requests_total_duration`.\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 this.lockKey = `metrics:http:v2:${keySeg}:drain_lock`\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 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 `2_redis_write pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} ok`\n )\n })\n } catch (e) {\n console.error('[HttpMetricsRedisStore] record:', e.message)\n }\n }\n\n /**\n * @param {(labels: Object, value: number) => void} applyCount\n * @param {(labels: Object, value: number) => void} applyDuration\n * @returns {Promise<boolean>}\n */\n flushToCounters(applyCount, applyDuration) {\n let client\n try {\n client = this._ensureClient()\n } catch (e) {\n console.error('[HttpMetricsRedisStore] flush:', e.message)\n return Promise.resolve(false)\n }\n return new Promise(resolve => {\n client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {\n if (setErr || ok !== 'OK') {\n httpMetricsTraceLog(\n `3_redis_drain_skip pid=${process.pid} countKey=${this.countKey} reason=${\n setErr ? `error:${setErr.message}` : 'lock_not_acquired'\n }`\n )\n resolve(false)\n return\n }\n client.eval(\n DRAIN_LUA,\n 2,\n this.countKey,\n this.durKey,\n (evalErr, raw) => {\n const finish = () => {\n client.del(this.lockKey, () => resolve(true))\n }\n\n if (evalErr) {\n console.error(\n '[HttpMetricsRedisStore] drain failed:',\n evalErr.message\n )\n finish()\n return\n }\n\n try {\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n httpMetricsTraceLog(\n `3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (empty_after_lua)`\n )\n finish()\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 `3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=${\n fieldKeys.length\n } sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`\n )\n } catch (e) {\n console.error(\n '[HttpMetricsRedisStore] flush apply failed:',\n e.message\n )\n }\n finish()\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":[]}
|
|
@@ -35,9 +35,8 @@ export function exitUnlessProcessTypeIn(metricsConfig: {
|
|
|
35
35
|
*
|
|
36
36
|
* **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,
|
|
37
37
|
* `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds
|
|
38
|
-
* — it does not run HTTP request metrics. HTTP Redis **key segment** is **`
|
|
39
|
-
* (
|
|
40
|
-
* identity for logs is often {@link METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR}.
|
|
38
|
+
* — it does not run HTTP request metrics. HTTP Redis **key segment** for API traffic is fixed **`web`**
|
|
39
|
+
* (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.
|
|
41
40
|
*
|
|
42
41
|
* @module metrics/metricsProcessTypeUtils
|
|
43
42
|
*/
|
|
@@ -51,8 +50,6 @@ export const METRICS_PROCESS_TYPE_REDIS: "redis-metrics";
|
|
|
51
50
|
export const METRICS_PROCESS_TYPE_WEB: "web";
|
|
52
51
|
/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */
|
|
53
52
|
export const METRICS_PROCESS_TYPE_WORKER: "worker";
|
|
54
|
-
/** HTTP Redis **drain + push** process (e.g. backend `http-metrics:` / `http-metrics-collector.ts`). */
|
|
55
|
-
export const METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR: "http-metrics";
|
|
56
53
|
/**
|
|
57
54
|
* Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).
|
|
58
55
|
* @type {readonly string[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metricsProcessTypeUtils.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsProcessTypeUtils.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metricsProcessTypeUtils.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsProcessTypeUtils.js"],"names":[],"mappings":"AAoCA;;;;;;GAMG;AACH,yDAJW;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,sBACxB,MAAM,GACJ,MAAM,CAQlB;AAED;;;;;;GAMG;AACH,uDAJW;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,uBACxB,MAAM,GACJ,IAAI,CAUhB;AAED;;;;;;;GAOG;AACH,uDALW;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,uBACxB,SAAS,MAAM,EAAE,0BACjB,MAAM,GACJ,IAAI,CAchB;AAxFD;;;;;;;;;;GAUG;AAEH,6DAA6D;AAC7D,+DAAwD;AAExD,gEAAgE;AAChE,yDAAkD;AAElD,yEAAyE;AACzE,yDAAkD;AAElD,kGAAkG;AAClG,6CAAsC;AAEtC,gGAAgG;AAChG,mDAA4C;AAE5C;;;GAGG;AACH,yDAFU,SAAS,MAAM,EAAE,CAKzB"}
|
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,
|
|
8
8
|
* `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds
|
|
9
|
-
* — it does not run HTTP request metrics. HTTP Redis **key segment** is **`
|
|
10
|
-
* (
|
|
11
|
-
* identity for logs is often {@link METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR}.
|
|
9
|
+
* — it does not run HTTP request metrics. HTTP Redis **key segment** for API traffic is fixed **`web`**
|
|
10
|
+
* (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.
|
|
12
11
|
*
|
|
13
12
|
* @module metrics/metricsProcessTypeUtils
|
|
14
13
|
*/
|
|
@@ -28,9 +27,6 @@ const METRICS_PROCESS_TYPE_WEB = 'web';
|
|
|
28
27
|
/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */
|
|
29
28
|
const METRICS_PROCESS_TYPE_WORKER = 'worker';
|
|
30
29
|
|
|
31
|
-
/** HTTP Redis **drain + push** process (e.g. backend `http-metrics:` / `http-metrics-collector.ts`). */
|
|
32
|
-
const METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR = 'http-metrics';
|
|
33
|
-
|
|
34
30
|
/**
|
|
35
31
|
* Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).
|
|
36
32
|
* @type {readonly string[]}
|
|
@@ -85,7 +81,6 @@ module.exports = {
|
|
|
85
81
|
METRICS_PROCESS_TYPE_REDIS,
|
|
86
82
|
METRICS_PROCESS_TYPE_WEB,
|
|
87
83
|
METRICS_PROCESS_TYPE_WORKER,
|
|
88
|
-
METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR,
|
|
89
84
|
REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES
|
|
90
85
|
};
|
|
91
86
|
//# sourceMappingURL=metricsProcessTypeUtils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metricsProcessTypeUtils.js","names":["METRICS_PROCESS_TYPE_DATABASE","METRICS_PROCESS_TYPE_QUEUE","METRICS_PROCESS_TYPE_REDIS","METRICS_PROCESS_TYPE_WEB","METRICS_PROCESS_TYPE_WORKER","
|
|
1
|
+
{"version":3,"file":"metricsProcessTypeUtils.js","names":["METRICS_PROCESS_TYPE_DATABASE","METRICS_PROCESS_TYPE_QUEUE","METRICS_PROCESS_TYPE_REDIS","METRICS_PROCESS_TYPE_WEB","METRICS_PROCESS_TYPE_WORKER","REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES","Object","freeze","resolveMetricsProcessType","metricsConfig","defaultProcessType","processType","process","env","BUILD_DYNO_PROCESS_TYPE","exitUnlessProcessTypeIs","expectedProcessType","resolved","exit","exitUnlessProcessTypeIn","allowedProcessTypes","defaultWhenUnspecified","includes","module","exports"],"sources":["../../src/metrics/metricsProcessTypeUtils.js"],"sourcesContent":["/**\n * Helpers for resolving `processType` and silently exiting when a specialized metrics client\n * is constructed on the wrong dyno / process (no log output).\n *\n * **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,\n * `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds\n * — it does not run HTTP request metrics. HTTP Redis **key segment** for API traffic is fixed **`web`**\n * (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.\n *\n * @module metrics/metricsProcessTypeUtils\n */\n\n/** DB-only metrics dyno (`database-metrics` in Procfile). */\nconst METRICS_PROCESS_TYPE_DATABASE = 'database-metrics'\n\n/** Queue + Redis metrics dyno (`queue-metrics` in Procfile). */\nconst METRICS_PROCESS_TYPE_QUEUE = 'queue-metrics'\n\n/** Redis-only metrics dyno (no Bee Queue; optional separate process). */\nconst METRICS_PROCESS_TYPE_REDIS = 'redis-metrics'\n\n/** Web servers — HTTP traffic, HTTP Redis **writers** typically use this in Redis key segment. */\nconst METRICS_PROCESS_TYPE_WEB = 'web'\n\n/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */\nconst METRICS_PROCESS_TYPE_WORKER = 'worker'\n\n/**\n * Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).\n * @type {readonly string[]}\n */\nconst REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES = Object.freeze([\n METRICS_PROCESS_TYPE_REDIS,\n METRICS_PROCESS_TYPE_QUEUE,\n])\n\n/**\n * Resolve logical process type the same way specialized metrics clients do.\n *\n * @param {{ processType?: string }} metricsConfig - Remainder of constructor options (e.g. after destructuring `redisClient`, `databaseUrl`, …)\n * @param {string} defaultProcessType - Fallback when `metricsConfig.processType` and `BUILD_DYNO_PROCESS_TYPE` are unset\n * @returns {string}\n */\nfunction resolveMetricsProcessType(metricsConfig, defaultProcessType) {\n return (\n metricsConfig.processType ||\n process.env.BUILD_DYNO_PROCESS_TYPE ||\n defaultProcessType\n )\n}\n\n/**\n * Exit with no logs if the resolved process type is not exactly `expectedProcessType`.\n *\n * @param {{ processType?: string }} metricsConfig\n * @param {string} expectedProcessType - Single allowed value (use a module constant, e.g. {@link METRICS_PROCESS_TYPE_DATABASE})\n * @returns {void}\n */\nfunction exitUnlessProcessTypeIs(metricsConfig, expectedProcessType) {\n const resolved = resolveMetricsProcessType(\n metricsConfig,\n expectedProcessType\n )\n if (resolved !== expectedProcessType) {\n process.exit(0)\n }\n}\n\n/**\n * Exit with no logs if the resolved process type is not in `allowedProcessTypes`.\n *\n * @param {{ processType?: string }} metricsConfig\n * @param {readonly string[]} allowedProcessTypes\n * @param {string} defaultWhenUnspecified - Used only to resolve when config/env omit `processType` (e.g. {@link METRICS_PROCESS_TYPE_QUEUE} for {@link RedisMetricsClient})\n * @returns {void}\n */\nfunction exitUnlessProcessTypeIn(\n metricsConfig,\n allowedProcessTypes,\n defaultWhenUnspecified\n) {\n const resolved = resolveMetricsProcessType(\n metricsConfig,\n defaultWhenUnspecified\n )\n if (!allowedProcessTypes.includes(resolved)) {\n process.exit(0)\n }\n}\n\nmodule.exports = {\n resolveMetricsProcessType,\n exitUnlessProcessTypeIs,\n exitUnlessProcessTypeIn,\n METRICS_PROCESS_TYPE_DATABASE,\n METRICS_PROCESS_TYPE_QUEUE,\n METRICS_PROCESS_TYPE_REDIS,\n METRICS_PROCESS_TYPE_WEB,\n METRICS_PROCESS_TYPE_WORKER,\n REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,\n}\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,MAAMA,6BAA6B,GAAG,kBAAkB;;AAExD;AACA,MAAMC,0BAA0B,GAAG,eAAe;;AAElD;AACA,MAAMC,0BAA0B,GAAG,eAAe;;AAElD;AACA,MAAMC,wBAAwB,GAAG,KAAK;;AAEtC;AACA,MAAMC,2BAA2B,GAAG,QAAQ;;AAE5C;AACA;AACA;AACA;AACA,MAAMC,0CAA0C,GAAGC,MAAM,CAACC,MAAM,CAAC,CAC/DL,0BAA0B,EAC1BD,0BAA0B,CAC3B,CAAC;;AAEF;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASO,yBAAyBA,CAACC,aAAa,EAAEC,kBAAkB,EAAE;EACpE,OACED,aAAa,CAACE,WAAW,IACzBC,OAAO,CAACC,GAAG,CAACC,uBAAuB,IACnCJ,kBAAkB;AAEtB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASK,uBAAuBA,CAACN,aAAa,EAAEO,mBAAmB,EAAE;EACnE,MAAMC,QAAQ,GAAGT,yBAAyB,CACxCC,aAAa,EACbO,mBACF,CAAC;EACD,IAAIC,QAAQ,KAAKD,mBAAmB,EAAE;IACpCJ,OAAO,CAACM,IAAI,CAAC,CAAC,CAAC;EACjB;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,uBAAuBA,CAC9BV,aAAa,EACbW,mBAAmB,EACnBC,sBAAsB,EACtB;EACA,MAAMJ,QAAQ,GAAGT,yBAAyB,CACxCC,aAAa,EACbY,sBACF,CAAC;EACD,IAAI,CAACD,mBAAmB,CAACE,QAAQ,CAACL,QAAQ,CAAC,EAAE;IAC3CL,OAAO,CAACM,IAAI,CAAC,CAAC,CAAC;EACjB;AACF;AAEAK,MAAM,CAACC,OAAO,GAAG;EACfhB,yBAAyB;EACzBO,uBAAuB;EACvBI,uBAAuB;EACvBnB,6BAA6B;EAC7BC,0BAA0B;EAC1BC,0BAA0B;EAC1BC,wBAAwB;EACxBC,2BAA2B;EAC3BC;AACF,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adalo/metrics",
|
|
3
|
-
"version": "0.0.0-staging.
|
|
3
|
+
"version": "0.0.0-staging.24",
|
|
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",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"@types/pg": "^8.15.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@babel/cli": "7.
|
|
35
|
-
"@babel/core": "7.
|
|
36
|
-
"@babel/preset-env": "7.
|
|
37
|
-
"@babel/preset-typescript": "7.
|
|
34
|
+
"@babel/cli": "7.24.7",
|
|
35
|
+
"@babel/core": "7.24.7",
|
|
36
|
+
"@babel/preset-env": "7.24.7",
|
|
37
|
+
"@babel/preset-typescript": "7.24.7",
|
|
38
38
|
"@types/ioredis": "^5.0.0",
|
|
39
39
|
"@types/jest": "28.1.6",
|
|
40
40
|
"@types/node": "17.0.38",
|
|
@@ -4,8 +4,9 @@ const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')
|
|
|
4
4
|
/**
|
|
5
5
|
* Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),
|
|
6
6
|
* applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.
|
|
8
|
+
* `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.
|
|
9
|
+
* Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).
|
|
9
10
|
*
|
|
10
11
|
* @extends BaseMetricsClient
|
|
11
12
|
*/
|
|
@@ -24,7 +25,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
24
25
|
* @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported
|
|
25
26
|
* @param {function} [config.startupValidation] Run before first push
|
|
26
27
|
* @param {boolean} [config.disablePushgateway] Skip POST to VM-agent
|
|
27
|
-
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default
|
|
28
|
+
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.
|
|
28
29
|
* @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)
|
|
29
30
|
*/
|
|
30
31
|
constructor(config = {}) {
|
|
@@ -38,10 +39,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
38
39
|
blockNodeDefaultMetrics: true,
|
|
39
40
|
})
|
|
40
41
|
|
|
41
|
-
const keyProcessType =
|
|
42
|
-
config.redisProcessTypeForKeys ||
|
|
43
|
-
process.env.METRICS_HTTP_REDIS_KEY_PROCESS_TYPE ||
|
|
44
|
-
'web'
|
|
42
|
+
const keyProcessType = config.redisProcessTypeForKeys || 'web'
|
|
45
43
|
|
|
46
44
|
this.defaultLabelsWithoutDynoId = {
|
|
47
45
|
app: this.appName,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')
|
|
1
|
+
const { HttpMetricsRedisStore, httpMetricsTraceLog } = require('./httpMetricsRedisStore')
|
|
2
|
+
|
|
3
|
+
function trunc(s, max = 120) {
|
|
4
|
+
const t = String(s)
|
|
5
|
+
return t.length > max ? `${t.slice(0, max - 3)}...` : t
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
@@ -45,6 +50,11 @@ class HttpMetricsRedisRecorder {
|
|
|
45
50
|
databaseId = '',
|
|
46
51
|
duration,
|
|
47
52
|
}) {
|
|
53
|
+
httpMetricsTraceLog(
|
|
54
|
+
`1_track pid=${process.pid} app=${this.appName} segment=${this.processType} ` +
|
|
55
|
+
`method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` +
|
|
56
|
+
`appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)}`
|
|
57
|
+
)
|
|
48
58
|
this._store.record(method, route, status_code, appId, databaseId, duration)
|
|
49
59
|
}
|
|
50
60
|
|
|
@@ -10,6 +10,33 @@ const FIELD_SEP = '\x1e'
|
|
|
10
10
|
*/
|
|
11
11
|
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120
|
|
12
12
|
|
|
13
|
+
const LOG = '[http-metrics-redis]'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* When Node runs behind `cluster`, HTTP is usually served from workers — include worker id in traces.
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function clusterHint() {
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line global-require
|
|
22
|
+
const c = require('cluster')
|
|
23
|
+
if (c.isWorker && c.worker != null) {
|
|
24
|
+
return ` cluster_worker=${c.worker.id}`
|
|
25
|
+
}
|
|
26
|
+
} catch (_) {
|
|
27
|
+
/* cluster not in use */
|
|
28
|
+
}
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stdout trace (uses **console.log**, not warn — same as typical app request logs).
|
|
34
|
+
* @param {string} body - Line body after `[http-metrics-redis]` prefix.
|
|
35
|
+
*/
|
|
36
|
+
function httpMetricsTraceLog(body) {
|
|
37
|
+
console.log(`${LOG} ${body}${clusterHint()}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
const DRAIN_LUA = `
|
|
14
41
|
local function drain(key)
|
|
15
42
|
local v = redis.call('HGETALL', key)
|
|
@@ -59,7 +86,9 @@ function hgetallPairsToObject(pairs) {
|
|
|
59
86
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.
|
|
60
87
|
*
|
|
61
88
|
* **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`
|
|
62
|
-
* (sum of duration ms per same field).
|
|
89
|
+
* (sum of duration ms per same field). Same `(method, route, status, appId, databaseId)` → same hash field;
|
|
90
|
+
* many requests in an interval **add** to count and **sum** durations (Redis `HINCRBY`). Drain applies those
|
|
91
|
+
* totals to `app_requests_total` / `app_requests_total_duration`.
|
|
63
92
|
*/
|
|
64
93
|
class HttpMetricsRedisStore {
|
|
65
94
|
/**
|
|
@@ -116,7 +145,11 @@ class HttpMetricsRedisStore {
|
|
|
116
145
|
.exec(err => {
|
|
117
146
|
if (err) {
|
|
118
147
|
console.error('[HttpMetricsRedisStore] record failed:', err.message)
|
|
148
|
+
return
|
|
119
149
|
}
|
|
150
|
+
httpMetricsTraceLog(
|
|
151
|
+
`2_redis_write pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} ok`
|
|
152
|
+
)
|
|
120
153
|
})
|
|
121
154
|
} catch (e) {
|
|
122
155
|
console.error('[HttpMetricsRedisStore] record:', e.message)
|
|
@@ -139,6 +172,11 @@ class HttpMetricsRedisStore {
|
|
|
139
172
|
return new Promise(resolve => {
|
|
140
173
|
client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {
|
|
141
174
|
if (setErr || ok !== 'OK') {
|
|
175
|
+
httpMetricsTraceLog(
|
|
176
|
+
`3_redis_drain_skip pid=${process.pid} countKey=${this.countKey} reason=${
|
|
177
|
+
setErr ? `error:${setErr.message}` : 'lock_not_acquired'
|
|
178
|
+
}`
|
|
179
|
+
)
|
|
142
180
|
resolve(false)
|
|
143
181
|
return
|
|
144
182
|
}
|
|
@@ -163,12 +201,17 @@ class HttpMetricsRedisStore {
|
|
|
163
201
|
|
|
164
202
|
try {
|
|
165
203
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
204
|
+
httpMetricsTraceLog(
|
|
205
|
+
`3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (empty_after_lua)`
|
|
206
|
+
)
|
|
166
207
|
finish()
|
|
167
208
|
return
|
|
168
209
|
}
|
|
169
210
|
const counts = hgetallPairsToObject(raw[0])
|
|
170
211
|
const durs = hgetallPairsToObject(raw[1])
|
|
171
212
|
const fieldKeys = Object.keys(counts)
|
|
213
|
+
let sumRequests = 0
|
|
214
|
+
const samples = []
|
|
172
215
|
for (const field of fieldKeys) {
|
|
173
216
|
const count = parseInt(counts[field], 10)
|
|
174
217
|
if (!count || count < 1) {
|
|
@@ -180,6 +223,10 @@ class HttpMetricsRedisStore {
|
|
|
180
223
|
continue
|
|
181
224
|
}
|
|
182
225
|
const [m, route, statusStr, aid, did] = parts
|
|
226
|
+
sumRequests += count
|
|
227
|
+
if (samples.length < 3) {
|
|
228
|
+
samples.push(`${m} ${route} ${statusStr} x${count}`)
|
|
229
|
+
}
|
|
183
230
|
const labels = {
|
|
184
231
|
method: m,
|
|
185
232
|
route,
|
|
@@ -192,6 +239,11 @@ class HttpMetricsRedisStore {
|
|
|
192
239
|
applyDuration(labels, dur)
|
|
193
240
|
}
|
|
194
241
|
}
|
|
242
|
+
httpMetricsTraceLog(
|
|
243
|
+
`3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=${
|
|
244
|
+
fieldKeys.length
|
|
245
|
+
} sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`
|
|
246
|
+
)
|
|
195
247
|
} catch (e) {
|
|
196
248
|
console.error(
|
|
197
249
|
'[HttpMetricsRedisStore] flush apply failed:',
|
|
@@ -212,4 +264,5 @@ module.exports = {
|
|
|
212
264
|
FIELD_SEP,
|
|
213
265
|
isRedisPeerInstalled,
|
|
214
266
|
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,
|
|
267
|
+
httpMetricsTraceLog,
|
|
215
268
|
}
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,
|
|
6
6
|
* `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds
|
|
7
|
-
* — it does not run HTTP request metrics. HTTP Redis **key segment** is **`
|
|
8
|
-
* (
|
|
9
|
-
* identity for logs is often {@link METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR}.
|
|
7
|
+
* — it does not run HTTP request metrics. HTTP Redis **key segment** for API traffic is fixed **`web`**
|
|
8
|
+
* (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.
|
|
10
9
|
*
|
|
11
10
|
* @module metrics/metricsProcessTypeUtils
|
|
12
11
|
*/
|
|
@@ -26,9 +25,6 @@ const METRICS_PROCESS_TYPE_WEB = 'web'
|
|
|
26
25
|
/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */
|
|
27
26
|
const METRICS_PROCESS_TYPE_WORKER = 'worker'
|
|
28
27
|
|
|
29
|
-
/** HTTP Redis **drain + push** process (e.g. backend `http-metrics:` / `http-metrics-collector.ts`). */
|
|
30
|
-
const METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR = 'http-metrics'
|
|
31
|
-
|
|
32
28
|
/**
|
|
33
29
|
* Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).
|
|
34
30
|
* @type {readonly string[]}
|
|
@@ -101,6 +97,5 @@ module.exports = {
|
|
|
101
97
|
METRICS_PROCESS_TYPE_REDIS,
|
|
102
98
|
METRICS_PROCESS_TYPE_WEB,
|
|
103
99
|
METRICS_PROCESS_TYPE_WORKER,
|
|
104
|
-
METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR,
|
|
105
100
|
REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,
|
|
106
101
|
}
|