@zintrust/queue-monitor 2.0.0 → 2.0.1
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/dist/api/workerClient.d.ts +20 -0
- package/dist/api/workerClient.js +45 -0
- package/dist/build-manifest.json +83 -7
- package/dist/config/queueMonitor.d.ts +18 -0
- package/dist/config/queueMonitor.js +21 -0
- package/dist/config/workerConfig.d.ts +3 -0
- package/dist/config/workerConfig.js +19 -0
- package/dist/dashboard-index.d.ts +5 -0
- package/dist/dashboard-index.js +5 -0
- package/dist/driver-index.d.ts +6 -0
- package/dist/driver-index.js +5 -0
- package/dist/metrics-index.d.ts +6 -0
- package/dist/metrics-index.js +5 -0
- package/dist/routes/workers.d.ts +10 -0
- package/dist/routes/workers.js +20 -0
- package/dist/runtime-index.d.ts +74 -0
- package/dist/runtime-index.js +234 -0
- package/dist/workers-ui.d.ts +7 -0
- package/dist/workers-ui.js +655 -0
- package/package.json +18 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type WorkerApiResponse<T> = {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
} & T;
|
|
5
|
+
export declare const WorkerClient: Readonly<{
|
|
6
|
+
listWorkers(): Promise<string[]>;
|
|
7
|
+
getWorker(name: string): Promise<unknown>;
|
|
8
|
+
getStatus(name: string): Promise<unknown>;
|
|
9
|
+
getHealth(name: string): Promise<unknown>;
|
|
10
|
+
startWorker(name: string): Promise<WorkerApiResponse<{
|
|
11
|
+
message?: string;
|
|
12
|
+
}>>;
|
|
13
|
+
stopWorker(name: string): Promise<WorkerApiResponse<{
|
|
14
|
+
message?: string;
|
|
15
|
+
}>>;
|
|
16
|
+
restartWorker(name: string): Promise<WorkerApiResponse<{
|
|
17
|
+
message?: string;
|
|
18
|
+
}>>;
|
|
19
|
+
}>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ErrorFactory, Logger } from '@zintrust/core';
|
|
2
|
+
import { WorkerConfig } from '../config/workerConfig.js';
|
|
3
|
+
const requestJson = async (path, options = {}) => {
|
|
4
|
+
const baseUrl = WorkerConfig.getWorkerBaseUrl();
|
|
5
|
+
const url = `${baseUrl}${path}`;
|
|
6
|
+
const response = await fetch(url, {
|
|
7
|
+
...options,
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
...options.headers,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
Logger.error('Worker API request failed', { url, status: response.status });
|
|
15
|
+
throw ErrorFactory.createWorkerError(`Worker API request failed (${response.status})`);
|
|
16
|
+
}
|
|
17
|
+
return (await response.json());
|
|
18
|
+
};
|
|
19
|
+
export const WorkerClient = Object.freeze({
|
|
20
|
+
async listWorkers() {
|
|
21
|
+
const response = await requestJson('/api/workers');
|
|
22
|
+
return response.workers ?? [];
|
|
23
|
+
},
|
|
24
|
+
async getWorker(name) {
|
|
25
|
+
const response = await requestJson(`/api/workers/${name}`);
|
|
26
|
+
return response.worker;
|
|
27
|
+
},
|
|
28
|
+
async getStatus(name) {
|
|
29
|
+
const response = await requestJson(`/api/workers/${name}/status`);
|
|
30
|
+
return response.status;
|
|
31
|
+
},
|
|
32
|
+
async getHealth(name) {
|
|
33
|
+
const response = await requestJson(`/api/workers/${name}/health`);
|
|
34
|
+
return response.health;
|
|
35
|
+
},
|
|
36
|
+
async startWorker(name) {
|
|
37
|
+
return requestJson(`/api/workers/${name}/start`, { method: 'POST' });
|
|
38
|
+
},
|
|
39
|
+
async stopWorker(name) {
|
|
40
|
+
return requestJson(`/api/workers/${name}/stop`, { method: 'POST' });
|
|
41
|
+
},
|
|
42
|
+
async restartWorker(name) {
|
|
43
|
+
return requestJson(`/api/workers/${name}/restart`, { method: 'POST' });
|
|
44
|
+
},
|
|
45
|
+
});
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-monitor",
|
|
3
3
|
"version": "2.0.0",
|
|
4
|
-
"buildDate": "2026-05-
|
|
4
|
+
"buildDate": "2026-05-22T14:51:47.293Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
|
-
"node": "
|
|
7
|
-
"platform": "
|
|
8
|
-
"arch": "
|
|
6
|
+
"node": "v22.22.1",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
12
|
-
"branch": "
|
|
11
|
+
"commit": "3c553e92",
|
|
12
|
+
"branch": "release"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -40,6 +40,34 @@
|
|
|
40
40
|
"size": 8928,
|
|
41
41
|
"sha256": "fc408db720d5883821b169eabce66944295068cd6f2338f1884493c299f0a0f1"
|
|
42
42
|
},
|
|
43
|
+
"api/workerClient.d.ts": {
|
|
44
|
+
"size": 597,
|
|
45
|
+
"sha256": "1d712bfa9127aa2df4f1fbd0efbb6d84069e1888ed73aca5542a41eee865d4bb"
|
|
46
|
+
},
|
|
47
|
+
"api/workerClient.js": {
|
|
48
|
+
"size": 1629,
|
|
49
|
+
"sha256": "60f993a42f4a9dd5000b01dae3b0f105a8f4348da8433a735b5cc1929a8f64ce"
|
|
50
|
+
},
|
|
51
|
+
"build-manifest.json": {
|
|
52
|
+
"size": 4136,
|
|
53
|
+
"sha256": "382a0dce0ff1c6077eece1a71780eb91726048370dc6a61fe9a80c1212917082"
|
|
54
|
+
},
|
|
55
|
+
"config/queueMonitor.d.ts": {
|
|
56
|
+
"size": 407,
|
|
57
|
+
"sha256": "4541f47e64c8ede1bfd8fc0cb7edb76c4e885311b28b1f51c9be7639e5d87eca"
|
|
58
|
+
},
|
|
59
|
+
"config/queueMonitor.js": {
|
|
60
|
+
"size": 689,
|
|
61
|
+
"sha256": "0b95e6b65d4b6ffdd69788cdfd19e0e76400a39ad69dce018d8827a3b298e419"
|
|
62
|
+
},
|
|
63
|
+
"config/workerConfig.d.ts": {
|
|
64
|
+
"size": 86,
|
|
65
|
+
"sha256": "b669205d50c8844455a2d9b34a54f48a71118eb6ac99bad5372683ab666f5a22"
|
|
66
|
+
},
|
|
67
|
+
"config/workerConfig.js": {
|
|
68
|
+
"size": 628,
|
|
69
|
+
"sha256": "ca1c6dbaa751893f0e6b7c8a7fd41a80f7d5e8fc9aaaa4877ca12821bb25f56f"
|
|
70
|
+
},
|
|
43
71
|
"connection.d.ts": {
|
|
44
72
|
"size": 285,
|
|
45
73
|
"sha256": "02b8d856e6b905f10ac01fabe7c44560530878c0ec73924889e1d159018d317a"
|
|
@@ -48,6 +76,14 @@
|
|
|
48
76
|
"size": 171,
|
|
49
77
|
"sha256": "a4d2967491dc471a75c163f81b5c7199f9e3b34c7c958b65d5bc6bb11798fc6f"
|
|
50
78
|
},
|
|
79
|
+
"dashboard-index.d.ts": {
|
|
80
|
+
"size": 180,
|
|
81
|
+
"sha256": "f78b4a0a83566640eca154f92d3f858ec41deb2521a500939ee3f0e58ee5db86"
|
|
82
|
+
},
|
|
83
|
+
"dashboard-index.js": {
|
|
84
|
+
"size": 183,
|
|
85
|
+
"sha256": "9d7c7fc63fe2919de0584d3f3d0ef74166d834af0a68caca2fe6fe64340652b9"
|
|
86
|
+
},
|
|
51
87
|
"dashboard-ui.d.ts": {
|
|
52
88
|
"size": 219,
|
|
53
89
|
"sha256": "5140191207b5620500ac5e1edacefed65956a1ad88739695d545ea7d2b405243"
|
|
@@ -56,6 +92,14 @@
|
|
|
56
92
|
"size": 53927,
|
|
57
93
|
"sha256": "71bab126f20aea1165ee3aa3c82e896ebd89b1f1a815ef2716cd884b846ab34f"
|
|
58
94
|
},
|
|
95
|
+
"driver-index.d.ts": {
|
|
96
|
+
"size": 280,
|
|
97
|
+
"sha256": "71a4dc5d823391fe16e966069aae8e6704d5c2d64aaceb447e138fdde7023690"
|
|
98
|
+
},
|
|
99
|
+
"driver-index.js": {
|
|
100
|
+
"size": 184,
|
|
101
|
+
"sha256": "fb91df973acd13a88cca443e50ecdd34944c66e1a1d48333f63099079a00f3c9"
|
|
102
|
+
},
|
|
59
103
|
"driver.d.ts": {
|
|
60
104
|
"size": 1102,
|
|
61
105
|
"sha256": "70b74e7a0489da3a5545dfd8458ab5b4def1f3733c291a782c033fed0afd0bcf"
|
|
@@ -70,7 +114,15 @@
|
|
|
70
114
|
},
|
|
71
115
|
"index.js": {
|
|
72
116
|
"size": 14399,
|
|
73
|
-
"sha256": "
|
|
117
|
+
"sha256": "150a5729d3250b7351a1282ce74d923ee6e0f43bee85deed86ad843f664521c4"
|
|
118
|
+
},
|
|
119
|
+
"metrics-index.d.ts": {
|
|
120
|
+
"size": 233,
|
|
121
|
+
"sha256": "993ffe832f2914a81c81364f7b6d5c7247cd16da9bb9feb04caf8d770c14f9ec"
|
|
122
|
+
},
|
|
123
|
+
"metrics-index.js": {
|
|
124
|
+
"size": 171,
|
|
125
|
+
"sha256": "53e858995605858b819f2c48c701e3b8f7d028ec51c1b4814c46284c7fb3989e"
|
|
74
126
|
},
|
|
75
127
|
"metrics.d.ts": {
|
|
76
128
|
"size": 910,
|
|
@@ -80,6 +132,22 @@
|
|
|
80
132
|
"size": 3472,
|
|
81
133
|
"sha256": "fab0e6d62ed694dcf57904a68b683280e4063c2fdcf97c60fdf5d933ae2b8f4f"
|
|
82
134
|
},
|
|
135
|
+
"routes/workers.d.ts": {
|
|
136
|
+
"size": 477,
|
|
137
|
+
"sha256": "cfcc3527c47d1a796a3ced2d4777b339dd767f7feb3ecc66c5ce0a91d8ff1bc8"
|
|
138
|
+
},
|
|
139
|
+
"routes/workers.js": {
|
|
140
|
+
"size": 901,
|
|
141
|
+
"sha256": "1ec48c0becc8c0628b1780fb400e957c5fe50da79f73112436c33ecde7d7d574"
|
|
142
|
+
},
|
|
143
|
+
"runtime-index.d.ts": {
|
|
144
|
+
"size": 2209,
|
|
145
|
+
"sha256": "e5d5c3475a7caec57f9e1a61bffb11038858e2c3f7be03e04ca28b8295e878fc"
|
|
146
|
+
},
|
|
147
|
+
"runtime-index.js": {
|
|
148
|
+
"size": 8759,
|
|
149
|
+
"sha256": "9c08789ef9e4da7cfcf431cd369808f9baa088381e58ada3daa5e91f6e034e5d"
|
|
150
|
+
},
|
|
83
151
|
"worker.d.ts": {
|
|
84
152
|
"size": 332,
|
|
85
153
|
"sha256": "97cddbc991c6e6724950090cd92f06671932ab848c16dd94a030d9fba3fbae26"
|
|
@@ -87,6 +155,14 @@
|
|
|
87
155
|
"worker.js": {
|
|
88
156
|
"size": 1233,
|
|
89
157
|
"sha256": "da26c1b80da7473e1644a4e0f8d814097ce0adff14e36037164f9ae5855e656a"
|
|
158
|
+
},
|
|
159
|
+
"workers-ui.d.ts": {
|
|
160
|
+
"size": 214,
|
|
161
|
+
"sha256": "4f5fd3bbd077d0dee2cc6c73d2c736fc52119b0d3a5f4a1878a4f3d3d3edf299"
|
|
162
|
+
},
|
|
163
|
+
"workers-ui.js": {
|
|
164
|
+
"size": 24928,
|
|
165
|
+
"sha256": "ba2c5cbf890fe875cc5336c76bd29b146089bc8ba3a70de4a2c383f395f9be73"
|
|
90
166
|
}
|
|
91
167
|
}
|
|
92
168
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Monitor Configuration (default override)
|
|
3
|
+
*
|
|
4
|
+
* Keep this file declarative:
|
|
5
|
+
* - Core owns env parsing/default logic.
|
|
6
|
+
* - Projects can override config by editing values below.
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
basePath: string;
|
|
11
|
+
middleware: string[];
|
|
12
|
+
redis: {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
password: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Env } from '@zintrust/core';
|
|
2
|
+
/**
|
|
3
|
+
* Queue Monitor Configuration (default override)
|
|
4
|
+
*
|
|
5
|
+
* Keep this file declarative:
|
|
6
|
+
* - Core owns env parsing/default logic.
|
|
7
|
+
* - Projects can override config by editing values below.
|
|
8
|
+
*/
|
|
9
|
+
export default {
|
|
10
|
+
enabled: Env.getBool('QUEUE_MONITOR_ENABLED', true),
|
|
11
|
+
basePath: Env.get('QUEUE_MONITOR_BASE_PATH', '/queue-monitor'),
|
|
12
|
+
middleware: Env.get('QUEUE_MONITOR_MIDDLEWARE', 'auth')
|
|
13
|
+
.split(',')
|
|
14
|
+
.map((m) => m.trim())
|
|
15
|
+
.filter((m) => m.length > 0),
|
|
16
|
+
redis: {
|
|
17
|
+
host: Env.get('REDIS_HOST', 'localhost'),
|
|
18
|
+
port: Env.getInt('REDIS_PORT', 6379),
|
|
19
|
+
password: Env.get('REDIS_PASSWORD', ''),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Env } from '@zintrust/core';
|
|
2
|
+
const normalizeBaseUrl = (value) => {
|
|
3
|
+
let end = value.length;
|
|
4
|
+
while (end > 0 && value.charAt(end - 1) === '/') {
|
|
5
|
+
end--;
|
|
6
|
+
}
|
|
7
|
+
return value.slice(0, end);
|
|
8
|
+
};
|
|
9
|
+
const withHttpScheme = (value) => value.startsWith('http://') || value.startsWith('https://') ? value : `http://${value}`;
|
|
10
|
+
const resolveWorkerApiUrl = () => {
|
|
11
|
+
const workerApiUrl = Env.get('WORKER_API_URL');
|
|
12
|
+
if (workerApiUrl) {
|
|
13
|
+
return normalizeBaseUrl(withHttpScheme(workerApiUrl));
|
|
14
|
+
}
|
|
15
|
+
return '';
|
|
16
|
+
};
|
|
17
|
+
export const WorkerConfig = Object.freeze({
|
|
18
|
+
getWorkerBaseUrl: resolveWorkerApiUrl,
|
|
19
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Monitor Driver - Runtime-only entrypoint
|
|
3
|
+
* Contains only the core queue driver functionality for production Workers
|
|
4
|
+
*/
|
|
5
|
+
export type { JobPayload, JobCounts, RetrySnapshot, RetryJobResult, QueueDriver } from './driver';
|
|
6
|
+
export { createBullMQDriver } from './driver';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type IRouter } from '@zintrust/core';
|
|
2
|
+
import { type WorkerUiOptions } from '../workers-ui';
|
|
3
|
+
type RouteOptions = {
|
|
4
|
+
middleware?: ReadonlyArray<string>;
|
|
5
|
+
} | undefined;
|
|
6
|
+
export declare const registerWorkerUiRoutes: (router: IRouter, options: WorkerUiOptions, routeOptions: RouteOptions) => void;
|
|
7
|
+
declare const _default: Readonly<{
|
|
8
|
+
registerWorkerUiRoutes: (router: IRouter, options: WorkerUiOptions, routeOptions: RouteOptions) => void;
|
|
9
|
+
}>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Router } from '@zintrust/core';
|
|
2
|
+
import { WorkerConfig } from '../config/workerConfig.js';
|
|
3
|
+
import { getWorkersHtml } from '../workers-ui.js';
|
|
4
|
+
const registerWorkerUiPage = (router, options, routeOptions) => {
|
|
5
|
+
const handler = (_req, res) => {
|
|
6
|
+
res.html(getWorkersHtml({
|
|
7
|
+
basePath: options.basePath,
|
|
8
|
+
apiBaseUrl: WorkerConfig.getWorkerBaseUrl(),
|
|
9
|
+
autoRefresh: options.autoRefresh,
|
|
10
|
+
refreshIntervalMs: options.refreshIntervalMs,
|
|
11
|
+
}));
|
|
12
|
+
};
|
|
13
|
+
Router.get(router, `${options.basePath}/workers`, handler, routeOptions);
|
|
14
|
+
Router.get(router, '/workers', handler, routeOptions);
|
|
15
|
+
Router.get(router, '/workers/', handler, routeOptions);
|
|
16
|
+
};
|
|
17
|
+
export const registerWorkerUiRoutes = (router, options, routeOptions) => {
|
|
18
|
+
registerWorkerUiPage(router, options, routeOptions);
|
|
19
|
+
};
|
|
20
|
+
export default Object.freeze({ registerWorkerUiRoutes });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Monitor - Runtime-only entrypoint for production Workers
|
|
3
|
+
* Excludes dashboard UI components that are not needed for runtime
|
|
4
|
+
*/
|
|
5
|
+
import { type RedisConfig } from './connection';
|
|
6
|
+
import { type QueueDriver } from './driver';
|
|
7
|
+
import { type Metrics } from './metrics';
|
|
8
|
+
export type { JobPayload } from './driver';
|
|
9
|
+
export { createMetrics, type JobStatus, type JobSummary, type Metrics } from './metrics';
|
|
10
|
+
export { createWorker as createQueueWorker, type QueueWorker } from './worker';
|
|
11
|
+
export type QueueMonitorConfig = {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
basePath?: string;
|
|
14
|
+
middleware?: ReadonlyArray<string>;
|
|
15
|
+
autoRefresh?: boolean;
|
|
16
|
+
refreshIntervalMs?: number;
|
|
17
|
+
redis?: RedisConfig;
|
|
18
|
+
knownQueues?: ReadonlyArray<string> | (() => Promise<ReadonlyArray<string>> | ReadonlyArray<string>);
|
|
19
|
+
};
|
|
20
|
+
export type QueueCounts = {
|
|
21
|
+
waiting: number;
|
|
22
|
+
active: number;
|
|
23
|
+
completed: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
delayed: number;
|
|
26
|
+
paused: number;
|
|
27
|
+
};
|
|
28
|
+
export type QueueMonitorSnapshot = {
|
|
29
|
+
status: 'ok';
|
|
30
|
+
startedAt: string;
|
|
31
|
+
queues: Array<{
|
|
32
|
+
name: string;
|
|
33
|
+
counts: QueueCounts;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
export type LockSummary = {
|
|
37
|
+
key: string;
|
|
38
|
+
ttl?: number;
|
|
39
|
+
expires?: string;
|
|
40
|
+
};
|
|
41
|
+
export type LockMetrics = {
|
|
42
|
+
active: number;
|
|
43
|
+
attempts: number;
|
|
44
|
+
acquired: number;
|
|
45
|
+
collisions: number;
|
|
46
|
+
collisionRate: number;
|
|
47
|
+
};
|
|
48
|
+
export type LockHistogramBucket = {
|
|
49
|
+
label: string;
|
|
50
|
+
count: number;
|
|
51
|
+
};
|
|
52
|
+
export type LockAnalytics = {
|
|
53
|
+
locks: LockSummary[];
|
|
54
|
+
metrics: LockMetrics;
|
|
55
|
+
histogram: LockHistogramBucket[];
|
|
56
|
+
};
|
|
57
|
+
export type QueueMonitorApi = {
|
|
58
|
+
getSnapshot: () => Promise<QueueMonitorSnapshot>;
|
|
59
|
+
getLocks: (pattern?: string) => Promise<LockAnalytics>;
|
|
60
|
+
driver: QueueDriver;
|
|
61
|
+
metrics: Metrics;
|
|
62
|
+
close: () => Promise<void>;
|
|
63
|
+
};
|
|
64
|
+
export declare const QueueMonitor: Readonly<{
|
|
65
|
+
create(config: QueueMonitorConfig): QueueMonitorApi;
|
|
66
|
+
}>;
|
|
67
|
+
export default QueueMonitor;
|
|
68
|
+
export { createBullMQDriver } from './driver';
|
|
69
|
+
/**
|
|
70
|
+
* Package version and build metadata
|
|
71
|
+
* Available at runtime for debugging and health checks
|
|
72
|
+
*/
|
|
73
|
+
export declare const _ZINTRUST_QUEUE_MONITOR_VERSION = "0.1.0";
|
|
74
|
+
export declare const _ZINTRUST_QUEUE_MONITOR_BUILD_DATE = "__BUILD_DATE__";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Monitor - Runtime-only entrypoint for production Workers
|
|
3
|
+
* Excludes dashboard UI components that are not needed for runtime
|
|
4
|
+
*/
|
|
5
|
+
import { isNonEmptyString, Logger, queueConfig, resolveLockPrefix, ShutdownTrace, } from '@zintrust/core';
|
|
6
|
+
import { createRedisConnection } from './connection';
|
|
7
|
+
import { createBullMQDriver } from './driver';
|
|
8
|
+
import { createMetrics } from './metrics';
|
|
9
|
+
export { createMetrics } from './metrics';
|
|
10
|
+
export { createWorker as createQueueWorker } from './worker';
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
basePath: '/queue-monitor',
|
|
14
|
+
middleware: [],
|
|
15
|
+
autoRefresh: true,
|
|
16
|
+
refreshIntervalMs: 5000,
|
|
17
|
+
};
|
|
18
|
+
const METRICS_KEYS = {
|
|
19
|
+
attempts: 'metrics:attempts',
|
|
20
|
+
acquired: 'metrics:acquired',
|
|
21
|
+
collisions: 'metrics:collisions',
|
|
22
|
+
};
|
|
23
|
+
const HISTOGRAM_BUCKETS = [
|
|
24
|
+
{ label: '<30s', max: 30_000 },
|
|
25
|
+
{ label: '30s-2m', max: 120_000 },
|
|
26
|
+
{ label: '2-10m', max: 600_000 },
|
|
27
|
+
{ label: '10-60m', max: 3_600_000 },
|
|
28
|
+
{ label: '>60m', min: 3_600_000 },
|
|
29
|
+
];
|
|
30
|
+
const MAX_LOCK_KEYS = 10_000;
|
|
31
|
+
function normalizeQueueNames(queueNames) {
|
|
32
|
+
return Array.from(new Set(queueNames
|
|
33
|
+
.filter((queueName) => typeof queueName === 'string' && isNonEmptyString(queueName))
|
|
34
|
+
.map((name) => name.trim())))
|
|
35
|
+
.filter((name) => name.length > 0)
|
|
36
|
+
.sort((left, right) => left.localeCompare(right));
|
|
37
|
+
}
|
|
38
|
+
async function resolveKnownQueues(knownQueues) {
|
|
39
|
+
if (typeof knownQueues === 'function') {
|
|
40
|
+
return normalizeQueueNames(await knownQueues());
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(knownQueues)) {
|
|
43
|
+
return normalizeQueueNames(knownQueues);
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
// Helper function to scan lock keys with pagination
|
|
48
|
+
const scanLockKeys = async (client, searchPattern, maxKeys) => {
|
|
49
|
+
const keys = [];
|
|
50
|
+
let cursor = '0';
|
|
51
|
+
do {
|
|
52
|
+
// Redis scan must be sequential
|
|
53
|
+
// eslint-disable-next-line no-await-in-loop
|
|
54
|
+
const [nextCursor, batch] = await client.scan(cursor, 'MATCH', searchPattern, 'COUNT', '200');
|
|
55
|
+
cursor = nextCursor;
|
|
56
|
+
keys.push(...batch);
|
|
57
|
+
if (keys.length >= maxKeys) {
|
|
58
|
+
Logger.warn('Lock scan limit reached', {
|
|
59
|
+
pattern: searchPattern,
|
|
60
|
+
keysFound: keys.length,
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
} while (cursor !== '0');
|
|
65
|
+
return keys;
|
|
66
|
+
};
|
|
67
|
+
// Helper function to get TTL statuses for keys
|
|
68
|
+
const getLockStatuses = async (client, keys) => {
|
|
69
|
+
return Promise.all(keys.map((key) => client.pttl(key)));
|
|
70
|
+
};
|
|
71
|
+
// Helper function to build lock objects from keys and statuses
|
|
72
|
+
const buildLockObjects = (keys, statuses, prefixLock) => {
|
|
73
|
+
return keys.map((key, index) => {
|
|
74
|
+
const ttl = statuses[index];
|
|
75
|
+
const exists = typeof ttl === 'number' && ttl > 0;
|
|
76
|
+
return {
|
|
77
|
+
key: key.replace(prefixLock, ''),
|
|
78
|
+
ttl: exists ? ttl : undefined,
|
|
79
|
+
expires: exists ? new Date(Date.now() + ttl).toISOString() : undefined,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
// Helper function to calculate lock metrics
|
|
84
|
+
const calculateLockMetrics = async (client, prefixLock) => {
|
|
85
|
+
const metricsKeys = [
|
|
86
|
+
`${prefixLock}${METRICS_KEYS.attempts}`,
|
|
87
|
+
`${prefixLock}${METRICS_KEYS.acquired}`,
|
|
88
|
+
`${prefixLock}${METRICS_KEYS.collisions}`,
|
|
89
|
+
];
|
|
90
|
+
const [attemptsRaw, acquiredRaw, collisionsRaw] = await client.mget(...metricsKeys);
|
|
91
|
+
const parseMetric = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
|
92
|
+
const attempts = parseMetric(attemptsRaw);
|
|
93
|
+
const acquired = parseMetric(acquiredRaw);
|
|
94
|
+
const collisions = parseMetric(collisionsRaw);
|
|
95
|
+
const collisionRate = attempts > 0 ? collisions / attempts : 0;
|
|
96
|
+
return { attempts, acquired, collisions, collisionRate };
|
|
97
|
+
};
|
|
98
|
+
// Helper function to build histogram from locks
|
|
99
|
+
const buildLockHistogram = (locks) => {
|
|
100
|
+
const histogram = HISTOGRAM_BUCKETS.map((bucket) => ({
|
|
101
|
+
label: bucket.label,
|
|
102
|
+
count: 0,
|
|
103
|
+
}));
|
|
104
|
+
locks.forEach((lock) => {
|
|
105
|
+
if (typeof lock.ttl !== 'number')
|
|
106
|
+
return;
|
|
107
|
+
const ttl = lock.ttl;
|
|
108
|
+
const idx = HISTOGRAM_BUCKETS.findIndex((bucket) => {
|
|
109
|
+
if (typeof bucket.min === 'number')
|
|
110
|
+
return ttl >= bucket.min;
|
|
111
|
+
if (typeof bucket.max === 'number')
|
|
112
|
+
return ttl < bucket.max;
|
|
113
|
+
return false;
|
|
114
|
+
});
|
|
115
|
+
if (idx >= 0)
|
|
116
|
+
histogram[idx].count += 1;
|
|
117
|
+
});
|
|
118
|
+
return histogram;
|
|
119
|
+
};
|
|
120
|
+
function createGetLocks(redisConfig) {
|
|
121
|
+
return async (pattern = '*') => {
|
|
122
|
+
const client = createRedisConnection(redisConfig, 3, { subsystem: 'queue-monitor-locks' });
|
|
123
|
+
const prefix_lock = resolveLockPrefix();
|
|
124
|
+
const searchPattern = `${prefix_lock}${pattern}`;
|
|
125
|
+
try {
|
|
126
|
+
// Scan for lock keys
|
|
127
|
+
const keys = await scanLockKeys(client, searchPattern, MAX_LOCK_KEYS);
|
|
128
|
+
// Get TTL statuses
|
|
129
|
+
const statuses = await getLockStatuses(client, keys);
|
|
130
|
+
// Build lock objects
|
|
131
|
+
const locks = buildLockObjects(keys, statuses, prefix_lock);
|
|
132
|
+
// Calculate metrics
|
|
133
|
+
const metrics = await calculateLockMetrics(client, prefix_lock);
|
|
134
|
+
// Build histogram
|
|
135
|
+
const histogram = buildLockHistogram(locks);
|
|
136
|
+
return {
|
|
137
|
+
locks,
|
|
138
|
+
metrics: {
|
|
139
|
+
active: locks.length,
|
|
140
|
+
...metrics,
|
|
141
|
+
},
|
|
142
|
+
histogram,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
if (typeof client.quit === 'function') {
|
|
147
|
+
await client.quit();
|
|
148
|
+
}
|
|
149
|
+
else if (typeof client.disconnect === 'function') {
|
|
150
|
+
client.disconnect();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function buildSettings(config) {
|
|
156
|
+
return {
|
|
157
|
+
enabled: config.enabled ?? DEFAULTS.enabled,
|
|
158
|
+
basePath: config.basePath ?? DEFAULTS.basePath,
|
|
159
|
+
middleware: config.middleware ?? DEFAULTS.middleware,
|
|
160
|
+
autoRefresh: config.autoRefresh ?? DEFAULTS.autoRefresh,
|
|
161
|
+
refreshIntervalMs: typeof config.refreshIntervalMs === 'number' && Number.isFinite(config.refreshIntervalMs)
|
|
162
|
+
? Math.max(1000, Math.floor(config.refreshIntervalMs))
|
|
163
|
+
: DEFAULTS.refreshIntervalMs,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function createGetSnapshot(driver, startedAt, knownQueues) {
|
|
167
|
+
return async () => {
|
|
168
|
+
const [discoveredQueues, persistedQueues] = await Promise.all([
|
|
169
|
+
driver.getQueues(),
|
|
170
|
+
resolveKnownQueues(knownQueues),
|
|
171
|
+
]);
|
|
172
|
+
const queues = Array.from(new Set([...persistedQueues, ...discoveredQueues])).sort((left, right) => left.localeCompare(right));
|
|
173
|
+
const stats = await Promise.all(queues.map(async (name) => {
|
|
174
|
+
const counts = await driver.getJobCounts(name);
|
|
175
|
+
return { name, counts: counts };
|
|
176
|
+
}));
|
|
177
|
+
return {
|
|
178
|
+
status: 'ok',
|
|
179
|
+
startedAt,
|
|
180
|
+
queues: stats,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export const QueueMonitor = Object.freeze({
|
|
185
|
+
create(config) {
|
|
186
|
+
const settings = buildSettings(config);
|
|
187
|
+
let redisConfig;
|
|
188
|
+
if (config?.redis) {
|
|
189
|
+
redisConfig = config?.redis;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
redisConfig = {
|
|
193
|
+
host: queueConfig.drivers.redis.host,
|
|
194
|
+
port: queueConfig.drivers.redis.port,
|
|
195
|
+
password: queueConfig.drivers.redis.password ?? '',
|
|
196
|
+
db: queueConfig.drivers.redis.database,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const driver = createBullMQDriver(redisConfig);
|
|
200
|
+
const metrics = createMetrics(redisConfig);
|
|
201
|
+
const startedAt = new Date().toISOString();
|
|
202
|
+
ShutdownTrace.logHandles('queue-monitor.create', {
|
|
203
|
+
basePath: settings.basePath,
|
|
204
|
+
autoRefresh: settings.autoRefresh,
|
|
205
|
+
refreshIntervalMs: settings.refreshIntervalMs,
|
|
206
|
+
});
|
|
207
|
+
const getSnapshot = createGetSnapshot(driver, startedAt, config.knownQueues);
|
|
208
|
+
const getLocks = createGetLocks(redisConfig);
|
|
209
|
+
const close = async () => {
|
|
210
|
+
ShutdownTrace.logHandles('queue-monitor.close.start', {
|
|
211
|
+
basePath: settings.basePath,
|
|
212
|
+
});
|
|
213
|
+
await Promise.all([driver.close(), metrics.close()]);
|
|
214
|
+
ShutdownTrace.logHandles('queue-monitor.close.complete', {
|
|
215
|
+
basePath: settings.basePath,
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
getSnapshot,
|
|
220
|
+
getLocks,
|
|
221
|
+
driver,
|
|
222
|
+
metrics,
|
|
223
|
+
close,
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
export default QueueMonitor;
|
|
228
|
+
export { createBullMQDriver } from './driver';
|
|
229
|
+
/**
|
|
230
|
+
* Package version and build metadata
|
|
231
|
+
* Available at runtime for debugging and health checks
|
|
232
|
+
*/
|
|
233
|
+
export const _ZINTRUST_QUEUE_MONITOR_VERSION = '0.1.0';
|
|
234
|
+
export const _ZINTRUST_QUEUE_MONITOR_BUILD_DATE = '__BUILD_DATE__';
|