@zintrust/queue-monitor 0.4.48 → 0.4.50
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/QueueMonitoringService.d.ts +5 -4
- package/dist/QueueMonitoringService.js +84 -72
- package/dist/api/workerClient.d.ts +20 -0
- package/dist/api/workerClient.js +45 -0
- package/dist/build-manifest.json +52 -8
- 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-ui.js +143 -16
- package/dist/index.d.ts +1 -0
- package/dist/index.js +27 -6
- package/dist/metrics.d.ts +1 -0
- package/dist/routes/workers.d.ts +10 -0
- package/dist/routes/workers.js +20 -0
- package/dist/workers-ui.d.ts +7 -0
- package/dist/workers-ui.js +655 -0
- package/package.json +3 -3
package/dist/dashboard-ui.js
CHANGED
|
@@ -239,7 +239,9 @@ const getDashboardBody = () => `
|
|
|
239
239
|
const getDashboardScriptState = (options) => String.raw `
|
|
240
240
|
const AUTO_REFRESH = ${options.autoRefresh ? 'true' : 'false'};
|
|
241
241
|
const REFRESH_INTERVAL = ${Math.max(1000, Math.floor(options.refreshIntervalMs || 0))};
|
|
242
|
+
const STREAM_RESET_MS = Math.max(15000, REFRESH_INTERVAL * 4);
|
|
242
243
|
const API_BASE = ${JSON.stringify(options.basePath)};
|
|
244
|
+
const ALL_QUEUES = '__all__';
|
|
243
245
|
const THEME_KEY = 'zintrust-queue-monitor-theme';
|
|
244
246
|
const AUTO_REFRESH_KEY = 'zintrust-queue-monitor-auto-refresh';
|
|
245
247
|
const QUEUE_KEY = 'zintrust-queue-monitor-selected-queue';
|
|
@@ -250,6 +252,10 @@ const getDashboardScriptState = (options) => String.raw `
|
|
|
250
252
|
let sseActive = false;
|
|
251
253
|
let lastSseQueue = null;
|
|
252
254
|
let lastSsePattern = null;
|
|
255
|
+
let reconnectTimer = null;
|
|
256
|
+
let streamWatchdogTimer = null;
|
|
257
|
+
let dashboardResetTimer = null;
|
|
258
|
+
let reconnectAttempts = 0;
|
|
253
259
|
let currentTheme = null;
|
|
254
260
|
`;
|
|
255
261
|
const getDashboardScriptTheme = () => `
|
|
@@ -308,6 +314,8 @@ const getDashboardScriptAutoRefresh = () => `
|
|
|
308
314
|
eventSource = null;
|
|
309
315
|
}
|
|
310
316
|
sseActive = false;
|
|
317
|
+
clearSseTimers();
|
|
318
|
+
clearError();
|
|
311
319
|
}
|
|
312
320
|
|
|
313
321
|
if (autoRefreshEnabled && !sseActive) {
|
|
@@ -389,18 +397,40 @@ const getRenderStatsFunction = () => `
|
|
|
389
397
|
const getUpdateQueueSelectFunction = () => `
|
|
390
398
|
function updateQueueSelect(queues) {
|
|
391
399
|
const select = document.getElementById('queue-select');
|
|
392
|
-
const
|
|
400
|
+
const storedQueue = localStorage.getItem(QUEUE_KEY);
|
|
401
|
+
const preferredQueue = currentQueue || select.value || storedQueue || '';
|
|
393
402
|
select.innerHTML = '';
|
|
394
403
|
|
|
395
|
-
if (queues.length === 0)
|
|
404
|
+
if (queues.length === 0) {
|
|
405
|
+
select.disabled = true;
|
|
406
|
+
select.innerHTML = '<option value="">No queues</option>';
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const queueNames = queues.map(q => q.name);
|
|
411
|
+
const totalWaiting = queues.reduce((acc, queue) => acc + queue.counts.waiting, 0);
|
|
412
|
+
const totalFailed = queues.reduce((acc, queue) => acc + queue.counts.failed, 0);
|
|
413
|
+
const allOption = document.createElement('option');
|
|
414
|
+
allOption.value = ALL_QUEUES;
|
|
415
|
+
allOption.textContent = 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed)';
|
|
416
|
+
|
|
417
|
+
const nextQueue = preferredQueue === ALL_QUEUES || queueNames.includes(preferredQueue)
|
|
418
|
+
? preferredQueue
|
|
419
|
+
: queueNames[0];
|
|
420
|
+
select.disabled = false;
|
|
421
|
+
allOption.selected = nextQueue === ALL_QUEUES;
|
|
422
|
+
select.appendChild(allOption);
|
|
396
423
|
|
|
397
424
|
queues.forEach(q => {
|
|
398
425
|
const opt = document.createElement('option');
|
|
399
426
|
opt.value = q.name;
|
|
400
427
|
opt.textContent = q.name + ' (' + q.counts.waiting + ' waiting, ' + q.counts.failed + ' failed)';
|
|
401
|
-
opt.selected = q.name ===
|
|
428
|
+
opt.selected = q.name === nextQueue;
|
|
402
429
|
select.appendChild(opt);
|
|
403
430
|
});
|
|
431
|
+
|
|
432
|
+
select.value = nextQueue;
|
|
433
|
+
return nextQueue;
|
|
404
434
|
}`;
|
|
405
435
|
const getRenderJobsFunction = () => `
|
|
406
436
|
// Track expanded job IDs to preserve state during SSE updates
|
|
@@ -634,6 +664,12 @@ const getErrorAndTooltipFunctions = () => `
|
|
|
634
664
|
el.style.display = 'block';
|
|
635
665
|
}
|
|
636
666
|
|
|
667
|
+
function clearError() {
|
|
668
|
+
const el = document.getElementById('error-container');
|
|
669
|
+
el.textContent = '';
|
|
670
|
+
el.style.display = 'none';
|
|
671
|
+
}
|
|
672
|
+
|
|
637
673
|
let tooltipEl = null;
|
|
638
674
|
function showTooltip(e) {
|
|
639
675
|
const info = e.target.getAttribute('data-info');
|
|
@@ -681,7 +717,7 @@ const getToggleDetailsFunctions = () => `
|
|
|
681
717
|
const jobData = {
|
|
682
718
|
id: job.id,
|
|
683
719
|
name: job.name,
|
|
684
|
-
queue: currentQueue,
|
|
720
|
+
queue: job.queue || currentQueue,
|
|
685
721
|
status: job.status || (job.failedReason ? 'failed' : 'completed'),
|
|
686
722
|
attempts: job.attempts,
|
|
687
723
|
timestamp: new Date(job.timestamp).toISOString(),
|
|
@@ -776,6 +812,77 @@ const getDashboardScriptHelpers = () => `
|
|
|
776
812
|
return patternInput && patternInput.value ? patternInput.value : '*';
|
|
777
813
|
}
|
|
778
814
|
|
|
815
|
+
function clearSseTimers() {
|
|
816
|
+
if (reconnectTimer !== null) {
|
|
817
|
+
clearTimeout(reconnectTimer);
|
|
818
|
+
reconnectTimer = null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (streamWatchdogTimer !== null) {
|
|
822
|
+
clearTimeout(streamWatchdogTimer);
|
|
823
|
+
streamWatchdogTimer = null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (dashboardResetTimer !== null) {
|
|
827
|
+
clearTimeout(dashboardResetTimer);
|
|
828
|
+
dashboardResetTimer = null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function scheduleDashboardReset(message) {
|
|
833
|
+
if (dashboardResetTimer !== null) return;
|
|
834
|
+
|
|
835
|
+
showError(message || 'Live updates stalled. Resetting dashboard...');
|
|
836
|
+
dashboardResetTimer = window.setTimeout(() => {
|
|
837
|
+
window.location.reload();
|
|
838
|
+
}, STREAM_RESET_MS);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function armStreamWatchdog() {
|
|
842
|
+
if (!autoRefreshEnabled) return;
|
|
843
|
+
|
|
844
|
+
if (streamWatchdogTimer !== null) {
|
|
845
|
+
clearTimeout(streamWatchdogTimer);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
streamWatchdogTimer = window.setTimeout(() => {
|
|
849
|
+
if (eventSource) {
|
|
850
|
+
eventSource.close();
|
|
851
|
+
eventSource = null;
|
|
852
|
+
}
|
|
853
|
+
sseActive = false;
|
|
854
|
+
streamWatchdogTimer = null;
|
|
855
|
+
scheduleDashboardReset('Live updates stalled. Resetting dashboard...');
|
|
856
|
+
}, STREAM_RESET_MS);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function markStreamHealthy() {
|
|
860
|
+
reconnectAttempts = 0;
|
|
861
|
+
if (reconnectTimer !== null) {
|
|
862
|
+
clearTimeout(reconnectTimer);
|
|
863
|
+
reconnectTimer = null;
|
|
864
|
+
}
|
|
865
|
+
clearError();
|
|
866
|
+
armStreamWatchdog();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function scheduleReconnect() {
|
|
870
|
+
if (!autoRefreshEnabled || reconnectTimer !== null) return;
|
|
871
|
+
|
|
872
|
+
reconnectAttempts += 1;
|
|
873
|
+
const delay = Math.min(1000 * reconnectAttempts, 5000);
|
|
874
|
+
showError('Live updates disconnected. Reconnecting...');
|
|
875
|
+
|
|
876
|
+
reconnectTimer = window.setTimeout(() => {
|
|
877
|
+
reconnectTimer = null;
|
|
878
|
+
setupEventStream(currentQueue);
|
|
879
|
+
}, delay);
|
|
880
|
+
|
|
881
|
+
if (reconnectAttempts >= 4) {
|
|
882
|
+
scheduleDashboardReset('Live updates could not reconnect. Resetting dashboard...');
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
779
886
|
function buildEventsUrl(queue, pattern) {
|
|
780
887
|
const q = queue || '';
|
|
781
888
|
const p = pattern || '*';
|
|
@@ -785,11 +892,12 @@ const getDashboardScriptHelpers = () => `
|
|
|
785
892
|
const getDashboardScriptEventStream = () => `
|
|
786
893
|
function setupEventStream(queueOverride) {
|
|
787
894
|
if (!window.EventSource) return;
|
|
895
|
+
if (!autoRefreshEnabled) return;
|
|
788
896
|
|
|
789
|
-
const queue = queueOverride
|
|
897
|
+
const queue = queueOverride === undefined ? currentQueue : queueOverride;
|
|
790
898
|
const pattern = getLockPattern();
|
|
791
899
|
|
|
792
|
-
if (eventSource && queue === lastSseQueue && pattern === lastSsePattern) return;
|
|
900
|
+
if (eventSource && sseActive && queue === lastSseQueue && pattern === lastSsePattern) return;
|
|
793
901
|
|
|
794
902
|
if (eventSource) {
|
|
795
903
|
eventSource.close();
|
|
@@ -799,10 +907,12 @@ const getDashboardScriptEventStream = () => `
|
|
|
799
907
|
lastSseQueue = queue;
|
|
800
908
|
lastSsePattern = pattern;
|
|
801
909
|
eventSource = new EventSource(buildEventsUrl(queue, pattern));
|
|
910
|
+
armStreamWatchdog();
|
|
802
911
|
|
|
803
912
|
eventSource.onopen = () => {
|
|
804
913
|
sseActive = true;
|
|
805
914
|
stopAutoRefresh();
|
|
915
|
+
markStreamHealthy();
|
|
806
916
|
};
|
|
807
917
|
|
|
808
918
|
eventSource.onmessage = (evt) => {
|
|
@@ -813,20 +923,31 @@ const getDashboardScriptEventStream = () => `
|
|
|
813
923
|
if (payload.type === 'snapshot') {
|
|
814
924
|
if (payload.snapshot) {
|
|
815
925
|
renderStats(payload.snapshot);
|
|
816
|
-
updateQueueSelect(payload.snapshot.queues || []);
|
|
926
|
+
const nextQueue = updateQueueSelect(payload.snapshot.queues || []);
|
|
927
|
+
if (nextQueue !== currentQueue) {
|
|
928
|
+
currentQueue = nextQueue;
|
|
929
|
+
if (currentQueue) {
|
|
930
|
+
localStorage.setItem(QUEUE_KEY, currentQueue);
|
|
931
|
+
} else {
|
|
932
|
+
localStorage.removeItem(QUEUE_KEY);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
817
935
|
}
|
|
818
936
|
|
|
819
|
-
if (
|
|
820
|
-
|
|
821
|
-
localStorage.setItem(QUEUE_KEY, currentQueue);
|
|
822
|
-
const select = document.getElementById('queue-select');
|
|
823
|
-
if (select) select.value = currentQueue;
|
|
937
|
+
if (!currentQueue) {
|
|
938
|
+
renderJobs([]);
|
|
824
939
|
}
|
|
825
940
|
|
|
826
|
-
if (payload.jobs
|
|
941
|
+
if (payload.jobs && (!payload.queue || payload.queue === currentQueue)) {
|
|
942
|
+
renderJobs(payload.jobs);
|
|
943
|
+
}
|
|
827
944
|
if (payload.locks) renderLocks(payload.locks);
|
|
828
945
|
|
|
829
|
-
document.getElementById('last-updated')
|
|
946
|
+
const lastUpdated = document.getElementById('last-updated');
|
|
947
|
+
if (lastUpdated) {
|
|
948
|
+
lastUpdated.textContent = new Date().toLocaleTimeString();
|
|
949
|
+
}
|
|
950
|
+
markStreamHealthy();
|
|
830
951
|
}
|
|
831
952
|
} catch (err) {
|
|
832
953
|
console.error('Failed to parse SSE payload', err);
|
|
@@ -839,7 +960,7 @@ const getDashboardScriptEventStream = () => `
|
|
|
839
960
|
eventSource = null;
|
|
840
961
|
}
|
|
841
962
|
sseActive = false;
|
|
842
|
-
|
|
963
|
+
scheduleReconnect();
|
|
843
964
|
};
|
|
844
965
|
}
|
|
845
966
|
`;
|
|
@@ -932,8 +1053,13 @@ const getDashboardScriptBootstrap = () => `
|
|
|
932
1053
|
if (queueSelect) {
|
|
933
1054
|
queueSelect.addEventListener('change', (e) => {
|
|
934
1055
|
currentQueue = e.target.value;
|
|
935
|
-
|
|
1056
|
+
if (currentQueue) {
|
|
1057
|
+
localStorage.setItem(QUEUE_KEY, currentQueue);
|
|
1058
|
+
} else {
|
|
1059
|
+
localStorage.removeItem(QUEUE_KEY);
|
|
1060
|
+
}
|
|
936
1061
|
console.log('Queue changed - SSE will update automatically');
|
|
1062
|
+
clearError();
|
|
937
1063
|
|
|
938
1064
|
setupEventStream(currentQueue);
|
|
939
1065
|
});
|
|
@@ -961,6 +1087,7 @@ const getDashboardScriptBootstrap = () => `
|
|
|
961
1087
|
setupEventStream(currentQueue);
|
|
962
1088
|
|
|
963
1089
|
window.addEventListener('beforeunload', () => {
|
|
1090
|
+
clearSseTimers();
|
|
964
1091
|
if (eventSource) {
|
|
965
1092
|
eventSource.close();
|
|
966
1093
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type QueueMonitorConfig = {
|
|
|
11
11
|
autoRefresh?: boolean;
|
|
12
12
|
refreshIntervalMs?: number;
|
|
13
13
|
redis?: RedisConfig;
|
|
14
|
+
knownQueues?: ReadonlyArray<string> | (() => Promise<ReadonlyArray<string>> | ReadonlyArray<string>);
|
|
14
15
|
};
|
|
15
16
|
export type QueueCounts = {
|
|
16
17
|
waiting: number;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Logger, queueConfig, resolveLockPrefix, Router, } from '@zintrust/core';
|
|
1
|
+
import { isNonEmptyString, Logger, queueConfig, resolveLockPrefix, Router, } from '@zintrust/core';
|
|
2
2
|
import { createRedisConnection } from './connection';
|
|
3
3
|
import { getDashboardHtml } from './dashboard-ui';
|
|
4
4
|
import { createBullMQDriver } from './driver';
|
|
5
5
|
import { createMetrics } from './metrics';
|
|
6
|
-
import {
|
|
6
|
+
import { getRecentJobsForSelection, QueueMonitoringStream } from './QueueMonitoringService';
|
|
7
7
|
export { createWorker as createQueueWorker } from './worker';
|
|
8
8
|
const DEFAULTS = {
|
|
9
9
|
enabled: true,
|
|
@@ -34,6 +34,23 @@ const HISTOGRAM_BUCKETS = [
|
|
|
34
34
|
{ label: '>60m', min: 3_600_000 },
|
|
35
35
|
];
|
|
36
36
|
const MAX_LOCK_KEYS = 10_000;
|
|
37
|
+
function normalizeQueueNames(queueNames) {
|
|
38
|
+
return Array.from(new Set(queueNames
|
|
39
|
+
.map((queueName) => queueName)
|
|
40
|
+
.filter(isNonEmptyString)
|
|
41
|
+
.map((name) => name.trim())))
|
|
42
|
+
.filter((name) => name.length > 0)
|
|
43
|
+
.sort((left, right) => left.localeCompare(right));
|
|
44
|
+
}
|
|
45
|
+
async function resolveKnownQueues(knownQueues) {
|
|
46
|
+
if (typeof knownQueues === 'function') {
|
|
47
|
+
return normalizeQueueNames(await knownQueues());
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(knownQueues)) {
|
|
50
|
+
return normalizeQueueNames(knownQueues);
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
37
54
|
// Helper function to scan lock keys with pagination
|
|
38
55
|
const scanLockKeys = async (client, searchPattern, maxKeys) => {
|
|
39
56
|
const keys = [];
|
|
@@ -148,7 +165,7 @@ async function handleJobsEndpoint(req, res, metrics, driver) {
|
|
|
148
165
|
res.status(400).json(fieldError('queue_name', 'Queue name must be provided'));
|
|
149
166
|
return;
|
|
150
167
|
}
|
|
151
|
-
const jobs = await
|
|
168
|
+
const jobs = await getRecentJobsForSelection(queueName, metrics, driver);
|
|
152
169
|
res.json(jobs);
|
|
153
170
|
}
|
|
154
171
|
async function handleRetryEndpoint(req, res, driver) {
|
|
@@ -177,9 +194,13 @@ function buildSettings(config) {
|
|
|
177
194
|
: DEFAULTS.refreshIntervalMs,
|
|
178
195
|
};
|
|
179
196
|
}
|
|
180
|
-
function createGetSnapshot(driver, startedAt) {
|
|
197
|
+
function createGetSnapshot(driver, startedAt, knownQueues) {
|
|
181
198
|
return async () => {
|
|
182
|
-
const
|
|
199
|
+
const [discoveredQueues, persistedQueues] = await Promise.all([
|
|
200
|
+
driver.getQueues(),
|
|
201
|
+
resolveKnownQueues(knownQueues),
|
|
202
|
+
]);
|
|
203
|
+
const queues = Array.from(new Set([...persistedQueues, ...discoveredQueues])).sort((left, right) => left.localeCompare(right));
|
|
183
204
|
const stats = await Promise.all(queues.map(async (name) => {
|
|
184
205
|
const counts = await driver.getJobCounts(name);
|
|
185
206
|
return { name, counts: counts };
|
|
@@ -270,7 +291,7 @@ export const QueueMonitor = Object.freeze({
|
|
|
270
291
|
const driver = createBullMQDriver(redisConfig);
|
|
271
292
|
const metrics = createMetrics(redisConfig);
|
|
272
293
|
const startedAt = new Date().toISOString();
|
|
273
|
-
const getSnapshot = createGetSnapshot(driver, startedAt);
|
|
294
|
+
const getSnapshot = createGetSnapshot(driver, startedAt, config.knownQueues);
|
|
274
295
|
const getLocks = createGetLocks(redisConfig);
|
|
275
296
|
const registerRoutes = createRegisterRoutes(settings, metrics, driver, getSnapshot, getLocks);
|
|
276
297
|
const close = async () => {
|
package/dist/metrics.d.ts
CHANGED
|
@@ -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 });
|