@adalo/metrics 0.1.173 → 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/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/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,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":[]}
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MetricsClient handles Prometheus metrics collection and push.
|
|
3
|
-
* Supports gauges,
|
|
4
|
-
*
|
|
3
|
+
* Supports gauges, default process metrics, optional HTTP counters, and custom metrics.
|
|
4
|
+
*
|
|
5
|
+
* **HTTP metrics:** In-process counters only (`app_requests_*`), gated by `httpMetricsEnabled` /
|
|
6
|
+
* `METRICS_HTTP_ENABLED`. For Redis-backed HTTP aggregation (multi-web / cluster), use
|
|
7
|
+
* {@link HttpMetricsRedisRecorder} and {@link HttpMetricsRedisCollector} — not this class.
|
|
8
|
+
*
|
|
9
|
+
* @extends BaseMetricsClient
|
|
5
10
|
*/
|
|
6
11
|
export class MetricsClient extends BaseMetricsClient {
|
|
7
12
|
/**
|
|
8
|
-
* @param {Object} config
|
|
13
|
+
* @param {Object} [config]
|
|
9
14
|
* @param {string} [config.appName] Name of the application
|
|
10
15
|
* @param {string} [config.dynoId] Dyno/instance ID
|
|
11
16
|
* @param {string} [config.processType] Process type (web, worker, etc.)
|
|
12
17
|
* @param {boolean} [config.enabled] Enable metrics collection
|
|
13
|
-
* @param {boolean} [config.httpMetricsEnabled
|
|
18
|
+
* @param {boolean} [config.httpMetricsEnabled] Enable HTTP request metrics (`app_requests_total`, `app_requests_total_duration`); defaults from `METRICS_HTTP_ENABLED === 'true'`
|
|
14
19
|
* @param {boolean} [config.logValues] Log metrics values to console
|
|
15
|
-
* @param {string} [config.pushgatewayUrl]
|
|
16
|
-
* @param {string} [config.pushgatewaySecret]
|
|
20
|
+
* @param {string} [config.pushgatewayUrl] Push URL (VM-agent import endpoint, e.g. .../api/v1/import/prometheus). /metrics is for GET (scrape), not POST (push).
|
|
21
|
+
* @param {string} [config.pushgatewaySecret] Basic auth secret (Base64 of `user:password`)
|
|
17
22
|
* @param {number} [config.intervalSec] Interval in seconds for pushing metrics
|
|
18
23
|
* @param {boolean} [config.removeOldMetrics] Enable to clear metrics by service name
|
|
19
|
-
* @param {function} [config.startupValidation] Add to validate on start push
|
|
20
|
-
* @param {boolean} [config.disablePushgateway] Disable pushing to
|
|
24
|
+
* @param {function} [config.startupValidation] Add to validate on start push
|
|
25
|
+
* @param {boolean} [config.disablePushgateway] Disable pushing to VM-agent (use HTTP scraping instead)
|
|
26
|
+
* @param {boolean} [config.blockNodeDefaultMetrics] When true, skip prom-client default process metrics (rare; see {@link BaseMetricsClient})
|
|
21
27
|
*/
|
|
22
28
|
constructor(config?: {
|
|
23
29
|
appName?: string | undefined;
|
|
@@ -32,7 +38,8 @@ export class MetricsClient extends BaseMetricsClient {
|
|
|
32
38
|
removeOldMetrics?: boolean | undefined;
|
|
33
39
|
startupValidation?: Function | undefined;
|
|
34
40
|
disablePushgateway?: boolean | undefined;
|
|
35
|
-
|
|
41
|
+
blockNodeDefaultMetrics?: boolean | undefined;
|
|
42
|
+
} | undefined);
|
|
36
43
|
httpMetricsEnabled: boolean;
|
|
37
44
|
_lastUsageMicros: number;
|
|
38
45
|
_lastCheckTime: number;
|
|
@@ -47,49 +54,49 @@ export class MetricsClient extends BaseMetricsClient {
|
|
|
47
54
|
*/
|
|
48
55
|
getCpuUsagePercent: () => number;
|
|
49
56
|
/**
|
|
50
|
-
*
|
|
57
|
+
* Available CPU cores (cgroup quota or `os.cpus().length`).
|
|
51
58
|
* @returns {number}
|
|
52
59
|
*/
|
|
53
60
|
getAvailableCPUs(): number;
|
|
54
61
|
/**
|
|
55
|
-
*
|
|
62
|
+
* Container memory usage in bytes (`memory.current` or RSS fallback).
|
|
56
63
|
* @returns {number}
|
|
57
64
|
*/
|
|
58
65
|
getContainerMemoryUsage(): number;
|
|
59
66
|
/**
|
|
60
|
-
*
|
|
67
|
+
* Container memory limit in bytes (`memory.max` or host total).
|
|
61
68
|
* @returns {number}
|
|
62
69
|
*/
|
|
63
70
|
getContainerMemoryLimit(): number;
|
|
64
71
|
/**
|
|
65
|
-
*
|
|
72
|
+
* Event loop lag sample in milliseconds.
|
|
66
73
|
* @returns {Promise<number>}
|
|
67
74
|
*/
|
|
68
75
|
measureLag(): Promise<number>;
|
|
69
76
|
/**
|
|
70
|
-
* Increment
|
|
77
|
+
* Increment HTTP request counters (in-process). No-op if `httpMetricsEnabled` is false.
|
|
71
78
|
*
|
|
72
|
-
* @param {Object} params
|
|
73
|
-
* @param {string} params.method
|
|
74
|
-
* @param {string} params.route
|
|
75
|
-
* @param {number} params.status_code
|
|
76
|
-
* @param {
|
|
77
|
-
* @param {string} [params.databaseId=''] - Optional database identifier.
|
|
78
|
-
* @param {number} params.duration - Request duration in milliseconds.
|
|
79
|
+
* @param {Object} params
|
|
80
|
+
* @param {string} params.method HTTP method
|
|
81
|
+
* @param {string} params.route Route or path pattern
|
|
82
|
+
* @param {number} params.status_code HTTP status code
|
|
83
|
+
* @param {number} params.duration Duration in milliseconds
|
|
79
84
|
*/
|
|
80
|
-
trackHttpRequest({ method, route, status_code,
|
|
85
|
+
trackHttpRequest({ method, route, status_code, duration }: {
|
|
81
86
|
method: string;
|
|
82
87
|
route: string;
|
|
83
88
|
status_code: number;
|
|
84
|
-
appId?: string | undefined;
|
|
85
|
-
databaseId?: string | undefined;
|
|
86
89
|
duration: number;
|
|
87
90
|
}): void;
|
|
88
91
|
/**
|
|
89
|
-
* Express middleware
|
|
90
|
-
*
|
|
92
|
+
* Express middleware: records `app_requests_*` on response finish.
|
|
93
|
+
* Skips when disabled, HTTP metrics off, or `OPTIONS`.
|
|
94
|
+
*
|
|
95
|
+
* @param {import('http').IncomingMessage} req
|
|
96
|
+
* @param {import('http').ServerResponse} res
|
|
97
|
+
* @param {function} next
|
|
91
98
|
*/
|
|
92
|
-
trackHttpRequestMiddleware: (req:
|
|
99
|
+
trackHttpRequestMiddleware: (req: import('http').IncomingMessage, res: import('http').ServerResponse, next: Function) => void;
|
|
93
100
|
}
|
|
94
101
|
import { BaseMetricsClient } from "./baseMetricsClient";
|
|
95
102
|
//# sourceMappingURL=metricsClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metricsClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsClient.js"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"metricsClient.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsClient.js"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH;IACE;;;;;;;;;;;;;;;OAeG;IACH;;;;;;;;;;;;;;mBAYC;IATC,4BAGiD;IAEjD,yBAAyB;IACzB,uBAAgC;IAKlC;;;OAGG;IACH,4BA4DC;IAED;;;OAGG;IACH,0BAFa,MAAM,CA2BlB;IAED;;;OAGG;IACH,oBAFa,MAAM,CAiBlB;IAED;;;OAGG;IACH,2BAFa,MAAM,CAWlB;IAED;;;OAGG;IACH,2BAFa,MAAM,CAgBlB;IAED;;;OAGG;IACH,cAFa,QAAQ,MAAM,CAAC,CAO3B;IAED;;;;;;;;OAQG;IACH;QAL0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACS,QAAQ,EAAvB,MAAM;aAkBhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAsBvC;CACF"}
|