@adalo/metrics 0.1.172 → 0.1.174
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/__tests__/httpMetricsRedisCollector.test.js +203 -0
- package/__tests__/httpMetricsRedisRecorder.test.js +60 -0
- package/__tests__/httpMetricsRedisStore.test.js +431 -0
- package/docs/http-metrics-redis.md +19 -0
- package/lib/health/healthCheckClient.js +1 -1
- package/lib/health/healthCheckClient.js.map +1 -1
- package/lib/health/healthCheckWorker.d.ts.map +1 -1
- package/lib/health/healthCheckWorker.js +15 -1
- package/lib/health/healthCheckWorker.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +44 -0
- package/lib/index.js.map +1 -1
- package/lib/metrics/baseMetricsClient.d.ts +2 -0
- package/lib/metrics/baseMetricsClient.d.ts.map +1 -1
- package/lib/metrics/baseMetricsClient.js +6 -3
- package/lib/metrics/baseMetricsClient.js.map +1 -1
- package/lib/metrics/httpMetricsRedisCollector.d.ts +50 -0
- package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -0
- package/lib/metrics/httpMetricsRedisCollector.js +117 -0
- package/lib/metrics/httpMetricsRedisCollector.js.map +1 -0
- package/lib/metrics/httpMetricsRedisRecorder.d.ts +48 -0
- package/lib/metrics/httpMetricsRedisRecorder.d.ts.map +1 -0
- package/lib/metrics/httpMetricsRedisRecorder.js +86 -0
- package/lib/metrics/httpMetricsRedisRecorder.js.map +1 -0
- package/lib/metrics/httpMetricsRedisStore.d.ts +88 -0
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -0
- package/lib/metrics/httpMetricsRedisStore.js +223 -0
- package/lib/metrics/httpMetricsRedisStore.js.map +1 -0
- package/lib/metrics/metricsClient.d.ts +34 -27
- package/lib/metrics/metricsClient.d.ts.map +1 -1
- package/lib/metrics/metricsClient.js +35 -37
- package/lib/metrics/metricsClient.js.map +1 -1
- package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -1
- package/lib/metrics/metricsDatabaseClient.js +6 -1
- package/lib/metrics/metricsDatabaseClient.js.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.d.ts +58 -0
- package/lib/metrics/metricsProcessTypeUtils.d.ts.map +1 -0
- package/lib/metrics/metricsProcessTypeUtils.js +86 -0
- package/lib/metrics/metricsProcessTypeUtils.js.map +1 -0
- package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -1
- package/lib/metrics/metricsQueueRedisClient.js +5 -0
- package/lib/metrics/metricsQueueRedisClient.js.map +1 -1
- package/lib/metrics/metricsRedisClient.d.ts.map +1 -1
- package/lib/metrics/metricsRedisClient.js +7 -1
- package/lib/metrics/metricsRedisClient.js.map +1 -1
- package/package.json +5 -5
- package/src/health/healthCheckClient.js +1 -1
- package/src/health/healthCheckWorker.js +18 -1
- package/src/index.ts +4 -0
- package/src/metrics/baseMetricsClient.js +4 -1
- package/src/metrics/httpMetricsRedisCollector.js +131 -0
- package/src/metrics/httpMetricsRedisRecorder.js +74 -0
- package/src/metrics/httpMetricsRedisStore.js +208 -0
- package/src/metrics/metricsClient.js +34 -53
- package/src/metrics/metricsDatabaseClient.js +7 -1
- package/src/metrics/metricsProcessTypeUtils.js +98 -0
- package/src/metrics/metricsQueueRedisClient.js +6 -0
- package/src/metrics/metricsRedisClient.js +12 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
BaseMetricsClient
|
|
5
|
+
} = require('./baseMetricsClient');
|
|
6
|
+
const {
|
|
7
|
+
HttpMetricsRedisStore,
|
|
8
|
+
buildFieldKey
|
|
9
|
+
} = require('./httpMetricsRedisStore');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),
|
|
13
|
+
* applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).
|
|
14
|
+
* **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.
|
|
15
|
+
* `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.
|
|
16
|
+
* Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).
|
|
17
|
+
*
|
|
18
|
+
* @extends BaseMetricsClient
|
|
19
|
+
*/
|
|
20
|
+
class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} [config]
|
|
23
|
+
* @param {import('redis').RedisClient} config.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).
|
|
24
|
+
* @param {string} [config.appName] Application name (defaults per {@link BaseMetricsClient})
|
|
25
|
+
* @param {string} [config.dynoId] Dyno/instance ID
|
|
26
|
+
* @param {string} [config.processType] Label `process_type` on push (default from env / base)
|
|
27
|
+
* @param {boolean} [config.enabled] Enable collection and push
|
|
28
|
+
* @param {boolean} [config.logValues] Log metric JSON to console
|
|
29
|
+
* @param {string} [config.pushgatewayUrl] VM-agent import URL
|
|
30
|
+
* @param {string} [config.pushgatewaySecret] Basic auth secret (Base64)
|
|
31
|
+
* @param {number} [config.intervalSec] Push interval (seconds)
|
|
32
|
+
* @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported
|
|
33
|
+
* @param {function} [config.startupValidation] Run before first push
|
|
34
|
+
* @param {boolean} [config.disablePushgateway] Skip POST to VM-agent
|
|
35
|
+
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.
|
|
36
|
+
* @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)
|
|
37
|
+
*/
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
const {
|
|
40
|
+
redisClient
|
|
41
|
+
} = config;
|
|
42
|
+
if (redisClient == null) {
|
|
43
|
+
throw new Error('HttpMetricsRedisCollector: redisClient is required');
|
|
44
|
+
}
|
|
45
|
+
super({
|
|
46
|
+
...config,
|
|
47
|
+
blockNodeDefaultMetrics: true
|
|
48
|
+
});
|
|
49
|
+
const keyProcessType = config.redisProcessTypeForKeys || 'web';
|
|
50
|
+
this.defaultLabelsWithoutDynoId = {
|
|
51
|
+
app: this.appName,
|
|
52
|
+
process_type: keyProcessType
|
|
53
|
+
};
|
|
54
|
+
this._store = new HttpMetricsRedisStore({
|
|
55
|
+
redisClient,
|
|
56
|
+
appName: this.appName,
|
|
57
|
+
processType: keyProcessType,
|
|
58
|
+
ttlSec: config.ttlSec
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** @type {Set<string>} Redis field keys already primed with inc(0) + push in this process */
|
|
62
|
+
this._httpCounterPrimedKeys = new Set();
|
|
63
|
+
this.createCounter({
|
|
64
|
+
name: 'app_requests_total',
|
|
65
|
+
help: 'Total number of HTTP requests',
|
|
66
|
+
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', 'status_code']),
|
|
67
|
+
useLabelsWithoutDynoId: true
|
|
68
|
+
});
|
|
69
|
+
this.createCounter({
|
|
70
|
+
name: 'app_requests_total_duration',
|
|
71
|
+
help: 'Total duration of HTTP requests in milliseconds',
|
|
72
|
+
labelNames: this.withDefaultLabelsWithoutDynoId(['method', 'route', 'status_code']),
|
|
73
|
+
useLabelsWithoutDynoId: true
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Drains Redis into counters, then runs gauge updates and VM-agent push ({@link BaseMetricsClient#_pushMetrics}).
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
pushMetrics = async () => {
|
|
82
|
+
if (this._store && this.countersFunctions?.app_requests_total && this.countersFunctions?.app_requests_total_duration) {
|
|
83
|
+
const {
|
|
84
|
+
ok,
|
|
85
|
+
rows
|
|
86
|
+
} = await this._store.drainRows();
|
|
87
|
+
if (ok && rows.length > 0) {
|
|
88
|
+
const applyCount = (labels, value) => this.countersFunctions.app_requests_total(labels, value);
|
|
89
|
+
const applyDur = (labels, value) => this.countersFunctions.app_requests_total_duration(labels, value);
|
|
90
|
+
let primedAny = false;
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const key = buildFieldKey(row.labels.method, row.labels.route, row.labels.status_code);
|
|
93
|
+
if (!this._httpCounterPrimedKeys.has(key)) {
|
|
94
|
+
this._httpCounterPrimedKeys.add(key);
|
|
95
|
+
applyCount(row.labels, 0);
|
|
96
|
+
applyDur(row.labels, 0);
|
|
97
|
+
primedAny = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (primedAny) {
|
|
101
|
+
await this.gatewayPush();
|
|
102
|
+
}
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
applyCount(row.labels, row.count);
|
|
105
|
+
if (row.dur > 0) {
|
|
106
|
+
applyDur(row.labels, row.dur);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return this._pushMetrics();
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
module.exports = {
|
|
115
|
+
HttpMetricsRedisCollector
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=httpMetricsRedisCollector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","buildFieldKey","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","defaultLabelsWithoutDynoId","app","appName","process_type","_store","processType","ttlSec","_httpCounterPrimedKeys","Set","createCounter","name","help","labelNames","withDefaultLabelsWithoutDynoId","useLabelsWithoutDynoId","pushMetrics","countersFunctions","app_requests_total","app_requests_total_duration","ok","rows","drainRows","length","applyCount","labels","value","applyDur","primedAny","row","key","method","route","status_code","has","add","gatewayPush","count","dur","_pushMetrics","module","exports"],"sources":["../../src/metrics/httpMetricsRedisCollector.js"],"sourcesContent":["const { BaseMetricsClient } = require('./baseMetricsClient')\nconst {\n HttpMetricsRedisStore,\n buildFieldKey,\n} = 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 /** @type {Set<string>} Redis field keys already primed with inc(0) + push in this process */\n this._httpCounterPrimedKeys = new Set()\n\n this.createCounter({\n name: 'app_requests_total',\n help: 'Total number of HTTP requests',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n 'status_code',\n ]),\n useLabelsWithoutDynoId: true,\n })\n\n this.createCounter({\n name: 'app_requests_total_duration',\n help: 'Total duration of HTTP requests in milliseconds',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n 'status_code',\n ]),\n useLabelsWithoutDynoId: true,\n })\n }\n\n /**\n * Drains Redis into counters, then runs gauge updates and VM-agent push ({@link BaseMetricsClient#_pushMetrics}).\n * @returns {Promise<void>}\n */\n pushMetrics = async () => {\n if (\n this._store &&\n this.countersFunctions?.app_requests_total &&\n this.countersFunctions?.app_requests_total_duration\n ) {\n const { ok, rows } = await this._store.drainRows()\n if (ok && rows.length > 0) {\n const applyCount = (labels, value) =>\n this.countersFunctions.app_requests_total(labels, value)\n const applyDur = (labels, value) =>\n this.countersFunctions.app_requests_total_duration(labels, value)\n\n let primedAny = false\n for (const row of rows) {\n const key = buildFieldKey(\n row.labels.method,\n row.labels.route,\n row.labels.status_code\n )\n if (!this._httpCounterPrimedKeys.has(key)) {\n this._httpCounterPrimedKeys.add(key)\n applyCount(row.labels, 0)\n applyDur(row.labels, 0)\n primedAny = true\n }\n }\n if (primedAny) {\n await this.gatewayPush()\n }\n for (const row of rows) {\n applyCount(row.labels, row.count)\n if (row.dur > 0) {\n applyDur(row.labels, row.dur)\n }\n }\n }\n }\n return this._pushMetrics()\n }\n}\n\nmodule.exports = { HttpMetricsRedisCollector }\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
3
|
+
* Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.
|
|
4
|
+
*
|
|
5
|
+
* @see HttpMetricsRedisStore
|
|
6
|
+
*/
|
|
7
|
+
export class HttpMetricsRedisRecorder {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} opts
|
|
10
|
+
* @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).
|
|
11
|
+
* @param {string} [opts.appName] Optional override; default same as {@link BaseMetricsClient}: `BUILD_APP_NAME` or `unknown-app`.
|
|
12
|
+
* @param {string} [opts.processType] Redis key segment (default **`web`**, same as {@link HttpMetricsRedisCollector}).
|
|
13
|
+
* @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).
|
|
14
|
+
*/
|
|
15
|
+
constructor({ redisClient, appName, processType, ttlSec }?: {
|
|
16
|
+
redisClient: import('redis').RedisClient;
|
|
17
|
+
appName?: string | undefined;
|
|
18
|
+
processType?: string | undefined;
|
|
19
|
+
ttlSec?: number | undefined;
|
|
20
|
+
});
|
|
21
|
+
processType: string;
|
|
22
|
+
appName: string;
|
|
23
|
+
_store: HttpMetricsRedisStore;
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} params
|
|
26
|
+
* @param {string} params.method
|
|
27
|
+
* @param {string} params.route
|
|
28
|
+
* @param {number} params.status_code
|
|
29
|
+
* @param {number} params.duration
|
|
30
|
+
*/
|
|
31
|
+
trackHttpRequest({ method, route, status_code, duration }: {
|
|
32
|
+
method: string;
|
|
33
|
+
route: string;
|
|
34
|
+
status_code: number;
|
|
35
|
+
duration: number;
|
|
36
|
+
}): void;
|
|
37
|
+
/**
|
|
38
|
+
* Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.
|
|
39
|
+
* Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.
|
|
40
|
+
*
|
|
41
|
+
* @param {import('http').IncomingMessage} req
|
|
42
|
+
* @param {import('http').ServerResponse} res
|
|
43
|
+
* @param {function} next
|
|
44
|
+
*/
|
|
45
|
+
trackHttpRequestMiddleware: (req: import('http').IncomingMessage, res: import('http').ServerResponse, next: Function) => void;
|
|
46
|
+
}
|
|
47
|
+
import { HttpMetricsRedisStore } from "./httpMetricsRedisStore";
|
|
48
|
+
//# sourceMappingURL=httpMetricsRedisRecorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACb,OAAO;QACP,WAAW;QACX,MAAM;OAgB9B;IARC,oBAA8B;IAC9B,gBAA8B;IAC9B,8BAKE;IAGJ;;;;;;OAMG;IACH;QAL0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACS,QAAQ,EAAvB,MAAM;aAIhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAsBvC;CACF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
HttpMetricsRedisStore
|
|
5
|
+
} = require('./httpMetricsRedisStore');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
9
|
+
* Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.
|
|
10
|
+
*
|
|
11
|
+
* @see HttpMetricsRedisStore
|
|
12
|
+
*/
|
|
13
|
+
class HttpMetricsRedisRecorder {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} opts
|
|
16
|
+
* @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).
|
|
17
|
+
* @param {string} [opts.appName] Optional override; default same as {@link BaseMetricsClient}: `BUILD_APP_NAME` or `unknown-app`.
|
|
18
|
+
* @param {string} [opts.processType] Redis key segment (default **`web`**, same as {@link HttpMetricsRedisCollector}).
|
|
19
|
+
* @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).
|
|
20
|
+
*/
|
|
21
|
+
constructor({
|
|
22
|
+
redisClient,
|
|
23
|
+
appName,
|
|
24
|
+
processType = 'web',
|
|
25
|
+
ttlSec
|
|
26
|
+
} = {}) {
|
|
27
|
+
if (redisClient == null) {
|
|
28
|
+
throw new Error('HttpMetricsRedisRecorder: redisClient is required');
|
|
29
|
+
}
|
|
30
|
+
const resolvedAppName = appName || process.env.BUILD_APP_NAME || 'unknown-app';
|
|
31
|
+
this.processType = processType;
|
|
32
|
+
this.appName = resolvedAppName;
|
|
33
|
+
this._store = new HttpMetricsRedisStore({
|
|
34
|
+
redisClient,
|
|
35
|
+
appName: resolvedAppName,
|
|
36
|
+
processType,
|
|
37
|
+
ttlSec
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Object} params
|
|
43
|
+
* @param {string} params.method
|
|
44
|
+
* @param {string} params.route
|
|
45
|
+
* @param {number} params.status_code
|
|
46
|
+
* @param {number} params.duration
|
|
47
|
+
*/
|
|
48
|
+
trackHttpRequest({
|
|
49
|
+
method,
|
|
50
|
+
route,
|
|
51
|
+
status_code,
|
|
52
|
+
duration
|
|
53
|
+
}) {
|
|
54
|
+
this._store.record(method, route, status_code, duration);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.
|
|
59
|
+
* Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.
|
|
60
|
+
*
|
|
61
|
+
* @param {import('http').IncomingMessage} req
|
|
62
|
+
* @param {import('http').ServerResponse} res
|
|
63
|
+
* @param {function} next
|
|
64
|
+
*/
|
|
65
|
+
trackHttpRequestMiddleware = (req, res, next) => {
|
|
66
|
+
if (req.method === 'OPTIONS') {
|
|
67
|
+
next();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const start = Date.now();
|
|
71
|
+
res.on('finish', () => {
|
|
72
|
+
const route = req.route?.path || req.path || 'unknown';
|
|
73
|
+
this.trackHttpRequest({
|
|
74
|
+
method: req.method,
|
|
75
|
+
route,
|
|
76
|
+
status_code: res.statusCode,
|
|
77
|
+
duration: Date.now() - start
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
next();
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
module.exports = {
|
|
84
|
+
HttpMetricsRedisRecorder
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=httpMetricsRedisRecorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.js","names":["HttpMetricsRedisStore","require","HttpMetricsRedisRecorder","constructor","redisClient","appName","processType","ttlSec","Error","resolvedAppName","process","env","BUILD_APP_NAME","_store","trackHttpRequest","method","route","status_code","duration","record","trackHttpRequestMiddleware","req","res","next","start","Date","now","on","path","statusCode","module","exports"],"sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"sourcesContent":["const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\n/**\n * Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).\n * Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.\n *\n * @see HttpMetricsRedisStore\n */\nclass HttpMetricsRedisRecorder {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} [opts.appName] Optional override; default same as {@link BaseMetricsClient}: `BUILD_APP_NAME` or `unknown-app`.\n * @param {string} [opts.processType] Redis key segment (default **`web`**, same as {@link HttpMetricsRedisCollector}).\n * @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).\n */\n constructor({ redisClient, appName, processType = 'web', ttlSec } = {}) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisRecorder: redisClient is required')\n }\n const resolvedAppName =\n appName || process.env.BUILD_APP_NAME || 'unknown-app'\n this.processType = processType\n this.appName = resolvedAppName\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName: resolvedAppName,\n processType,\n ttlSec,\n })\n }\n\n /**\n * @param {Object} params\n * @param {string} params.method\n * @param {string} params.route\n * @param {number} params.status_code\n * @param {number} params.duration\n */\n trackHttpRequest({ method, route, status_code, duration }) {\n this._store.record(method, route, status_code, duration)\n }\n\n /**\n * Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.\n * Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.\n *\n * @param {import('http').IncomingMessage} req\n * @param {import('http').ServerResponse} res\n * @param {function} next\n */\n trackHttpRequestMiddleware = (req, res, next) => {\n if (req.method === 'OPTIONS') {\n next()\n return\n }\n\n const start = Date.now()\n res.on('finish', () => {\n const route = req.route?.path || req.path || 'unknown'\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { HttpMetricsRedisRecorder }\n"],"mappings":";;AAAA,MAAM;EAAEA;AAAsB,CAAC,GAAGC,OAAO,CAAC,yBAAyB,CAAC;;AAEpE;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,wBAAwB,CAAC;EAC7B;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAAC;IAAEC,WAAW;IAAEC,OAAO;IAAEC,WAAW,GAAG,KAAK;IAAEC;EAAO,CAAC,GAAG,CAAC,CAAC,EAAE;IACtE,IAAIH,WAAW,IAAI,IAAI,EAAE;MACvB,MAAM,IAAII,KAAK,CAAC,mDAAmD,CAAC;IACtE;IACA,MAAMC,eAAe,GACnBJ,OAAO,IAAIK,OAAO,CAACC,GAAG,CAACC,cAAc,IAAI,aAAa;IACxD,IAAI,CAACN,WAAW,GAAGA,WAAW;IAC9B,IAAI,CAACD,OAAO,GAAGI,eAAe;IAC9B,IAAI,CAACI,MAAM,GAAG,IAAIb,qBAAqB,CAAC;MACtCI,WAAW;MACXC,OAAO,EAAEI,eAAe;MACxBH,WAAW;MACXC;IACF,CAAC,CAAC;EACJ;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;EACEO,gBAAgBA,CAAC;IAAEC,MAAM;IAAEC,KAAK;IAAEC,WAAW;IAAEC;EAAS,CAAC,EAAE;IACzD,IAAI,CAACL,MAAM,CAACM,MAAM,CAACJ,MAAM,EAAEC,KAAK,EAAEC,WAAW,EAAEC,QAAQ,CAAC;EAC1D;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACEE,0BAA0B,GAAGA,CAACC,GAAG,EAAEC,GAAG,EAAEC,IAAI,KAAK;IAC/C,IAAIF,GAAG,CAACN,MAAM,KAAK,SAAS,EAAE;MAC5BQ,IAAI,CAAC,CAAC;MACN;IACF;IAEA,MAAMC,KAAK,GAAGC,IAAI,CAACC,GAAG,CAAC,CAAC;IACxBJ,GAAG,CAACK,EAAE,CAAC,QAAQ,EAAE,MAAM;MACrB,MAAMX,KAAK,GAAGK,GAAG,CAACL,KAAK,EAAEY,IAAI,IAAIP,GAAG,CAACO,IAAI,IAAI,SAAS;MAEtD,IAAI,CAACd,gBAAgB,CAAC;QACpBC,MAAM,EAAEM,GAAG,CAACN,MAAM;QAClBC,KAAK;QACLC,WAAW,EAAEK,GAAG,CAACO,UAAU;QAC3BX,QAAQ,EAAEO,IAAI,CAACC,GAAG,CAAC,CAAC,GAAGF;MACzB,CAAC,CAAC;IACJ,CAAC,CAAC;IAEFD,IAAI,CAAC,CAAC;EACR,CAAC;AACH;AAEAO,MAAM,CAACC,OAAO,GAAG;EAAE7B;AAAyB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
3
|
+
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.
|
|
4
|
+
*
|
|
5
|
+
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes
|
|
6
|
+
* `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.
|
|
7
|
+
*/
|
|
8
|
+
export class HttpMetricsRedisStore {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} opts
|
|
11
|
+
* @param {import('redis').RedisClient} opts.redisClient
|
|
12
|
+
* @param {string} opts.appName BUILD_APP_NAME (key segment)
|
|
13
|
+
* @param {string} opts.processType logical process for key (e.g. web)
|
|
14
|
+
* @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)
|
|
15
|
+
*/
|
|
16
|
+
constructor({ redisClient, appName, processType, ttlSec }: {
|
|
17
|
+
redisClient: import('redis').RedisClient;
|
|
18
|
+
appName: string;
|
|
19
|
+
processType: string;
|
|
20
|
+
ttlSec?: number | undefined;
|
|
21
|
+
});
|
|
22
|
+
_client: import("redis").RedisClient;
|
|
23
|
+
ttlSec: number;
|
|
24
|
+
countKey: string;
|
|
25
|
+
durKey: string;
|
|
26
|
+
/**
|
|
27
|
+
* @returns {import('redis').RedisClient}
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
private _ensureClient;
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} method
|
|
33
|
+
* @param {string} route
|
|
34
|
+
* @param {number} statusCode
|
|
35
|
+
* @param {number} durationMs
|
|
36
|
+
*/
|
|
37
|
+
record(method: string, route: string, statusCode: number, durationMs: number): void;
|
|
38
|
+
/**
|
|
39
|
+
* Atomically drain Redis hashes (same Lua as `flushToCounters`) and return parsed rows.
|
|
40
|
+
*
|
|
41
|
+
* @returns {Promise<{ ok: boolean, rows: { labels: Object, count: number, dur: number }[] }>}
|
|
42
|
+
*/
|
|
43
|
+
drainRows(): Promise<{
|
|
44
|
+
ok: boolean;
|
|
45
|
+
rows: {
|
|
46
|
+
labels: Object;
|
|
47
|
+
count: number;
|
|
48
|
+
dur: number;
|
|
49
|
+
}[];
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* @param {(labels: Object, value: number) => void} applyCount
|
|
53
|
+
* @param {(labels: Object, value: number) => void} applyDuration
|
|
54
|
+
* @returns {Promise<boolean>}
|
|
55
|
+
*/
|
|
56
|
+
flushToCounters(applyCount: (labels: Object, value: number) => void, applyDuration: (labels: Object, value: number) => void): Promise<boolean>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} method
|
|
60
|
+
* @param {string} route
|
|
61
|
+
* @param {number} statusCode
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function buildFieldKey(method: string, route: string, statusCode: number): string;
|
|
65
|
+
/**
|
|
66
|
+
* Record separator for hash fields (avoids collisions when route contains "|").
|
|
67
|
+
* @type {string}
|
|
68
|
+
*/
|
|
69
|
+
export const FIELD_SEP: string;
|
|
70
|
+
/**
|
|
71
|
+
* Default Redis key TTL in seconds (sliding: refreshed on each `record` write).
|
|
72
|
+
* @type {number}
|
|
73
|
+
*/
|
|
74
|
+
export const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC: number;
|
|
75
|
+
/**
|
|
76
|
+
* @param {unknown} raw redis eval result [countPairs, durPairs]
|
|
77
|
+
* @returns {{ labels: { method: string, route: string, status_code: string }, count: number, dur: number }[]}
|
|
78
|
+
*/
|
|
79
|
+
export function rowsFromDrainRaw(raw: unknown): {
|
|
80
|
+
labels: {
|
|
81
|
+
method: string;
|
|
82
|
+
route: string;
|
|
83
|
+
status_code: string;
|
|
84
|
+
};
|
|
85
|
+
count: number;
|
|
86
|
+
dur: number;
|
|
87
|
+
}[];
|
|
88
|
+
//# sourceMappingURL=httpMetricsRedisStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"AA6EA;;;;;;GAMG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACd,OAAO,EAApB,MAAM;QACO,WAAW,EAAxB,MAAM;QACQ,MAAM;OAgB9B;IAVC,qCAA0B;IAC1B,eAGwC;IAIxC,iBAAiD;IACjD,eAA6C;IAG/C;;;OAGG;IACH,sBAEC;IAED;;;;;OAKG;IACH,eALW,MAAM,SACN,MAAM,cACN,MAAM,cACN,MAAM,QAqBhB;IAED;;;;OAIG;IACH,aAFa,QAAQ;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC,CAgC5F;IAED;;;;OAIG;IACH,qCAJoB,MAAM,SAAS,MAAM,KAAK,IAAI,0BAC9B,MAAM,SAAS,MAAM,KAAK,IAAI,GACrC,QAAQ,OAAO,CAAC,CAe5B;CACF;AAlLD;;;;;GAKG;AACH,sCALW,MAAM,SACN,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AA7BD;;;GAGG;AACH,wBAFU,MAAM,CAEQ;AAExB;;;GAGG;AACH,iDAFU,MAAM,CAE8B;AAgC9C;;;GAGG;AACH,sCAHW,OAAO,GACL;IAAE,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,CA+B5G"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Record separator for hash fields (avoids collisions when route contains "|").
|
|
5
|
+
* @type {string}
|
|
6
|
+
*/
|
|
7
|
+
const FIELD_SEP = '\x1e';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default Redis key TTL in seconds (sliding: refreshed on each `record` write).
|
|
11
|
+
* @type {number}
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120;
|
|
14
|
+
const DRAIN_LUA = `
|
|
15
|
+
local function drain(key)
|
|
16
|
+
local v = redis.call('HGETALL', key)
|
|
17
|
+
redis.call('DEL', key)
|
|
18
|
+
return v
|
|
19
|
+
end
|
|
20
|
+
return {drain(KEYS[1]), drain(KEYS[2])}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} method
|
|
25
|
+
* @param {string} route
|
|
26
|
+
* @param {number} statusCode
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function buildFieldKey(method, route, statusCode) {
|
|
30
|
+
return [method, route, String(statusCode)].join(FIELD_SEP);
|
|
31
|
+
}
|
|
32
|
+
function hgetallPairsToObject(pairs) {
|
|
33
|
+
const o = {};
|
|
34
|
+
if (!pairs || !pairs.length) {
|
|
35
|
+
return o;
|
|
36
|
+
}
|
|
37
|
+
for (let i = 0; i < pairs.length; i += 2) {
|
|
38
|
+
o[pairs[i]] = pairs[i + 1];
|
|
39
|
+
}
|
|
40
|
+
return o;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {unknown} raw redis eval result [countPairs, durPairs]
|
|
45
|
+
* @returns {{ labels: { method: string, route: string, status_code: string }, count: number, dur: number }[]}
|
|
46
|
+
*/
|
|
47
|
+
function rowsFromDrainRaw(raw) {
|
|
48
|
+
const rows = [];
|
|
49
|
+
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
50
|
+
return rows;
|
|
51
|
+
}
|
|
52
|
+
const counts = hgetallPairsToObject(raw[0]);
|
|
53
|
+
const durs = hgetallPairsToObject(raw[1]);
|
|
54
|
+
const fieldKeys = Object.keys(counts);
|
|
55
|
+
for (const field of fieldKeys) {
|
|
56
|
+
const count = parseInt(counts[field], 10);
|
|
57
|
+
if (!count || count < 1) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const dur = parseInt(durs[field] || '0', 10) || 0;
|
|
61
|
+
const parts = field.split(FIELD_SEP);
|
|
62
|
+
let labels = null;
|
|
63
|
+
if (parts.length === 3) {
|
|
64
|
+
const [m, route, statusStr] = parts;
|
|
65
|
+
labels = {
|
|
66
|
+
method: m,
|
|
67
|
+
route,
|
|
68
|
+
status_code: statusStr
|
|
69
|
+
};
|
|
70
|
+
} else if (parts.length === 5 || parts.length === 6) {
|
|
71
|
+
const [m, route, statusStr] = parts;
|
|
72
|
+
labels = {
|
|
73
|
+
method: m,
|
|
74
|
+
route,
|
|
75
|
+
status_code: statusStr
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!labels) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
rows.push({
|
|
82
|
+
labels,
|
|
83
|
+
count,
|
|
84
|
+
dur
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return rows;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
92
|
+
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.
|
|
93
|
+
*
|
|
94
|
+
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes
|
|
95
|
+
* `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.
|
|
96
|
+
*/
|
|
97
|
+
class HttpMetricsRedisStore {
|
|
98
|
+
/**
|
|
99
|
+
* @param {Object} opts
|
|
100
|
+
* @param {import('redis').RedisClient} opts.redisClient
|
|
101
|
+
* @param {string} opts.appName BUILD_APP_NAME (key segment)
|
|
102
|
+
* @param {string} opts.processType logical process for key (e.g. web)
|
|
103
|
+
* @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)
|
|
104
|
+
*/
|
|
105
|
+
constructor({
|
|
106
|
+
redisClient,
|
|
107
|
+
appName,
|
|
108
|
+
processType,
|
|
109
|
+
ttlSec
|
|
110
|
+
}) {
|
|
111
|
+
if (redisClient == null) {
|
|
112
|
+
throw new Error('HttpMetricsRedisStore: redisClient is required');
|
|
113
|
+
}
|
|
114
|
+
this._client = redisClient;
|
|
115
|
+
this.ttlSec = typeof ttlSec === 'number' && ttlSec > 0 ? ttlSec : DEFAULT_HTTP_METRICS_REDIS_TTL_SEC;
|
|
116
|
+
const keySeg = `${encodeURIComponent(appName)}:${encodeURIComponent(processType)}`;
|
|
117
|
+
this.countKey = `metrics:http:v2:${keySeg}:count`;
|
|
118
|
+
this.durKey = `metrics:http:v2:${keySeg}:dur`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @returns {import('redis').RedisClient}
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
_ensureClient() {
|
|
126
|
+
return this._client;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} method
|
|
131
|
+
* @param {string} route
|
|
132
|
+
* @param {number} statusCode
|
|
133
|
+
* @param {number} durationMs
|
|
134
|
+
*/
|
|
135
|
+
record(method, route, statusCode, durationMs) {
|
|
136
|
+
try {
|
|
137
|
+
const client = this._ensureClient();
|
|
138
|
+
const field = buildFieldKey(method, route, statusCode);
|
|
139
|
+
const dur = Math.max(0, Math.round(Number(durationMs) || 0));
|
|
140
|
+
client.multi().hincrby(this.countKey, field, 1).hincrby(this.durKey, field, dur).expire(this.countKey, this.ttlSec).expire(this.durKey, this.ttlSec).exec(err => {
|
|
141
|
+
if (err) {
|
|
142
|
+
console.error('[HttpMetricsRedisStore] record failed:', err.message);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.error('[HttpMetricsRedisStore] record:', e.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Atomically drain Redis hashes (same Lua as `flushToCounters`) and return parsed rows.
|
|
152
|
+
*
|
|
153
|
+
* @returns {Promise<{ ok: boolean, rows: { labels: Object, count: number, dur: number }[] }>}
|
|
154
|
+
*/
|
|
155
|
+
drainRows() {
|
|
156
|
+
let client;
|
|
157
|
+
try {
|
|
158
|
+
client = this._ensureClient();
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error('[HttpMetricsRedisStore] drainRows:', e.message);
|
|
161
|
+
return Promise.resolve({
|
|
162
|
+
ok: false,
|
|
163
|
+
rows: []
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return new Promise(resolve => {
|
|
167
|
+
client.eval(DRAIN_LUA, 2, this.countKey, this.durKey, (evalErr, raw) => {
|
|
168
|
+
if (evalErr) {
|
|
169
|
+
console.error('[HttpMetricsRedisStore] drain failed:', evalErr.message);
|
|
170
|
+
resolve({
|
|
171
|
+
ok: false,
|
|
172
|
+
rows: []
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const rows = rowsFromDrainRaw(raw);
|
|
178
|
+
resolve({
|
|
179
|
+
ok: true,
|
|
180
|
+
rows
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error('[HttpMetricsRedisStore] drainRows parse failed:', e.message);
|
|
184
|
+
resolve({
|
|
185
|
+
ok: false,
|
|
186
|
+
rows: []
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {(labels: Object, value: number) => void} applyCount
|
|
195
|
+
* @param {(labels: Object, value: number) => void} applyDuration
|
|
196
|
+
* @returns {Promise<boolean>}
|
|
197
|
+
*/
|
|
198
|
+
flushToCounters(applyCount, applyDuration) {
|
|
199
|
+
return this.drainRows().then(({
|
|
200
|
+
ok,
|
|
201
|
+
rows
|
|
202
|
+
}) => {
|
|
203
|
+
if (!ok) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
applyCount(row.labels, row.count);
|
|
208
|
+
if (row.dur > 0) {
|
|
209
|
+
applyDuration(row.labels, row.dur);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
module.exports = {
|
|
217
|
+
HttpMetricsRedisStore,
|
|
218
|
+
buildFieldKey,
|
|
219
|
+
FIELD_SEP,
|
|
220
|
+
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,
|
|
221
|
+
rowsFromDrainRaw
|
|
222
|
+
};
|
|
223
|
+
//# sourceMappingURL=httpMetricsRedisStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","DRAIN_LUA","buildFieldKey","method","route","statusCode","String","join","hgetallPairsToObject","pairs","o","length","i","rowsFromDrainRaw","raw","rows","Array","isArray","counts","durs","fieldKeys","Object","keys","field","count","parseInt","dur","parts","split","labels","m","statusStr","status_code","push","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","_ensureClient","record","durationMs","client","Math","max","round","Number","multi","hincrby","expire","exec","err","console","error","message","e","drainRows","Promise","resolve","ok","eval","evalErr","flushToCounters","applyCount","applyDuration","then","row","module","exports"],"sources":["../../src/metrics/httpMetricsRedisStore.js"],"sourcesContent":["/**\n * Record separator for hash fields (avoids collisions when route contains \"|\").\n * @type {string}\n */\nconst FIELD_SEP = '\\x1e'\n\n/**\n * Default Redis key TTL in seconds (sliding: refreshed on each `record` write).\n * @type {number}\n */\nconst DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120\n\nconst DRAIN_LUA = `\nlocal function drain(key)\n local v = redis.call('HGETALL', key)\n redis.call('DEL', key)\n return v\nend\nreturn {drain(KEYS[1]), drain(KEYS[2])}\n`\n\n/**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @returns {string}\n */\nfunction buildFieldKey(method, route, statusCode) {\n return [method, route, String(statusCode)].join(FIELD_SEP)\n}\n\nfunction hgetallPairsToObject(pairs) {\n const o = {}\n if (!pairs || !pairs.length) {\n return o\n }\n for (let i = 0; i < pairs.length; i += 2) {\n o[pairs[i]] = pairs[i + 1]\n }\n return o\n}\n\n/**\n * @param {unknown} raw redis eval result [countPairs, durPairs]\n * @returns {{ labels: { method: string, route: string, status_code: string }, count: number, dur: number }[]}\n */\nfunction rowsFromDrainRaw(raw) {\n const rows = []\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n return rows\n }\n const counts = hgetallPairsToObject(raw[0])\n const durs = hgetallPairsToObject(raw[1])\n const fieldKeys = Object.keys(counts)\n for (const field of fieldKeys) {\n const count = parseInt(counts[field], 10)\n if (!count || count < 1) {\n continue\n }\n const dur = parseInt(durs[field] || '0', 10) || 0\n const parts = field.split(FIELD_SEP)\n let labels = null\n if (parts.length === 3) {\n const [m, route, statusStr] = parts\n labels = { method: m, route, status_code: statusStr }\n } else if (parts.length === 5 || parts.length === 6) {\n const [m, route, statusStr] = parts\n labels = { method: m, route, status_code: statusStr }\n }\n if (!labels) {\n continue\n }\n rows.push({ labels, count, dur })\n }\n return rows\n}\n\n/**\n * Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).\n * Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.\n *\n * **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes\n * `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.\n */\nclass HttpMetricsRedisStore {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient\n * @param {string} opts.appName BUILD_APP_NAME (key segment)\n * @param {string} opts.processType logical process for key (e.g. web)\n * @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisStore: redisClient is required')\n }\n this._client = redisClient\n this.ttlSec =\n typeof ttlSec === 'number' && ttlSec > 0\n ? ttlSec\n : DEFAULT_HTTP_METRICS_REDIS_TTL_SEC\n const keySeg = `${encodeURIComponent(appName)}:${encodeURIComponent(\n processType\n )}`\n this.countKey = `metrics:http:v2:${keySeg}:count`\n this.durKey = `metrics:http:v2:${keySeg}:dur`\n }\n\n /**\n * @returns {import('redis').RedisClient}\n * @private\n */\n _ensureClient() {\n return this._client\n }\n\n /**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {number} durationMs\n */\n record(method, route, statusCode, durationMs) {\n try {\n const client = this._ensureClient()\n const field = buildFieldKey(method, route, statusCode)\n const dur = Math.max(0, Math.round(Number(durationMs) || 0))\n client\n .multi()\n .hincrby(this.countKey, field, 1)\n .hincrby(this.durKey, field, dur)\n .expire(this.countKey, this.ttlSec)\n .expire(this.durKey, this.ttlSec)\n .exec(err => {\n if (err) {\n console.error('[HttpMetricsRedisStore] record failed:', err.message)\n }\n })\n } catch (e) {\n console.error('[HttpMetricsRedisStore] record:', e.message)\n }\n }\n\n /**\n * Atomically drain Redis hashes (same Lua as `flushToCounters`) and return parsed rows.\n *\n * @returns {Promise<{ ok: boolean, rows: { labels: Object, count: number, dur: number }[] }>}\n */\n drainRows() {\n let client\n try {\n client = this._ensureClient()\n } catch (e) {\n console.error('[HttpMetricsRedisStore] drainRows:', e.message)\n return Promise.resolve({ ok: false, rows: [] })\n }\n return new Promise(resolve => {\n client.eval(DRAIN_LUA, 2, this.countKey, this.durKey, (evalErr, raw) => {\n if (evalErr) {\n console.error(\n '[HttpMetricsRedisStore] drain failed:',\n evalErr.message\n )\n resolve({ ok: false, rows: [] })\n return\n }\n try {\n const rows = rowsFromDrainRaw(raw)\n resolve({ ok: true, rows })\n } catch (e) {\n console.error(\n '[HttpMetricsRedisStore] drainRows parse failed:',\n e.message\n )\n resolve({ ok: false, rows: [] })\n }\n })\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 return this.drainRows().then(({ ok, rows }) => {\n if (!ok) {\n return false\n }\n for (const row of rows) {\n applyCount(row.labels, row.count)\n if (row.dur > 0) {\n applyDuration(row.labels, row.dur)\n }\n }\n return true\n })\n }\n}\n\nmodule.exports = {\n HttpMetricsRedisStore,\n buildFieldKey,\n FIELD_SEP,\n DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,\n rowsFromDrainRaw,\n}\n"],"mappings":"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","ignoreList":[]}
|