queen-mq 0.1.0
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/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- package/src/websocket/wsServer.js +221 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
export const createAnalyticsRoutes = (queueManager) => {
|
|
2
|
+
|
|
3
|
+
const getQueues = async (filters = {}) => {
|
|
4
|
+
// Extract and pass all filter parameters
|
|
5
|
+
const { fromDateTime, toDateTime, queue, namespace, task } = filters;
|
|
6
|
+
const stats = await queueManager.getQueueStats({
|
|
7
|
+
fromDateTime,
|
|
8
|
+
toDateTime,
|
|
9
|
+
queue,
|
|
10
|
+
namespace,
|
|
11
|
+
task
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Group by queue for aggregation
|
|
15
|
+
const queueMap = new Map();
|
|
16
|
+
|
|
17
|
+
stats.forEach(row => {
|
|
18
|
+
if (!queueMap.has(row.queue)) {
|
|
19
|
+
queueMap.set(row.queue, {
|
|
20
|
+
queue: row.queue,
|
|
21
|
+
namespace: row.namespace,
|
|
22
|
+
task: row.task,
|
|
23
|
+
partitions: [],
|
|
24
|
+
totals: {
|
|
25
|
+
pending: 0,
|
|
26
|
+
processing: 0,
|
|
27
|
+
completed: 0,
|
|
28
|
+
failed: 0,
|
|
29
|
+
deadLetter: 0,
|
|
30
|
+
total: 0
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const queueData = queueMap.get(row.queue);
|
|
36
|
+
|
|
37
|
+
// Create stats object from row data
|
|
38
|
+
const stats = {
|
|
39
|
+
pending: parseInt(row.pending || 0),
|
|
40
|
+
processing: parseInt(row.processing || 0),
|
|
41
|
+
completed: parseInt(row.completed || 0),
|
|
42
|
+
failed: parseInt(row.failed || 0),
|
|
43
|
+
deadLetter: parseInt(row.dead_letter || 0),
|
|
44
|
+
total: parseInt(row.total_messages || 0)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
queueData.partitions.push({
|
|
48
|
+
name: row.partition,
|
|
49
|
+
stats: stats
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Aggregate totals
|
|
53
|
+
Object.keys(stats).forEach(key => {
|
|
54
|
+
if (queueData.totals[key] !== undefined) {
|
|
55
|
+
queueData.totals[key] += stats[key];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return { queues: Array.from(queueMap.values()) };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getQueueStats = async (queueName, filters = {}) => {
|
|
64
|
+
const { fromDateTime, toDateTime } = filters;
|
|
65
|
+
const stats = await queueManager.getQueueStats({
|
|
66
|
+
queue: queueName,
|
|
67
|
+
fromDateTime,
|
|
68
|
+
toDateTime
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const totals = {
|
|
72
|
+
pending: 0,
|
|
73
|
+
processing: 0,
|
|
74
|
+
completed: 0,
|
|
75
|
+
failed: 0,
|
|
76
|
+
deadLetter: 0,
|
|
77
|
+
total: 0
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const partitions = stats.map(row => {
|
|
81
|
+
// Create stats object from row data
|
|
82
|
+
const stats = {
|
|
83
|
+
pending: parseInt(row.pending || 0),
|
|
84
|
+
processing: parseInt(row.processing || 0),
|
|
85
|
+
completed: parseInt(row.completed || 0),
|
|
86
|
+
failed: parseInt(row.failed || 0),
|
|
87
|
+
deadLetter: parseInt(row.dead_letter || 0),
|
|
88
|
+
total: parseInt(row.total_messages || 0)
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
Object.keys(stats).forEach(key => {
|
|
92
|
+
if (totals[key] !== undefined) {
|
|
93
|
+
totals[key] += stats[key];
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
name: row.partition,
|
|
99
|
+
stats: stats
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
queue: queueName,
|
|
105
|
+
namespace: stats[0]?.namespace,
|
|
106
|
+
task: stats[0]?.task,
|
|
107
|
+
totals,
|
|
108
|
+
partitions
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getNamespaceStats = async (namespace, filters = {}) => {
|
|
113
|
+
const { fromDateTime, toDateTime } = filters;
|
|
114
|
+
const stats = await queueManager.getQueueStats({
|
|
115
|
+
namespace,
|
|
116
|
+
fromDateTime,
|
|
117
|
+
toDateTime
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Group by queue
|
|
121
|
+
const queueMap = new Map();
|
|
122
|
+
|
|
123
|
+
stats.forEach(row => {
|
|
124
|
+
if (!queueMap.has(row.queue)) {
|
|
125
|
+
queueMap.set(row.queue, {
|
|
126
|
+
queue: row.queue,
|
|
127
|
+
task: row.task,
|
|
128
|
+
partitions: [],
|
|
129
|
+
totals: {
|
|
130
|
+
pending: 0,
|
|
131
|
+
processing: 0,
|
|
132
|
+
completed: 0,
|
|
133
|
+
failed: 0,
|
|
134
|
+
deadLetter: 0,
|
|
135
|
+
total: 0
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const queueData = queueMap.get(row.queue);
|
|
141
|
+
|
|
142
|
+
// Create stats object from row data
|
|
143
|
+
const stats = {
|
|
144
|
+
pending: parseInt(row.pending || 0),
|
|
145
|
+
processing: parseInt(row.processing || 0),
|
|
146
|
+
completed: parseInt(row.completed || 0),
|
|
147
|
+
failed: parseInt(row.failed || 0),
|
|
148
|
+
deadLetter: parseInt(row.dead_letter || 0),
|
|
149
|
+
total: parseInt(row.total_messages || 0)
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
queueData.partitions.push({
|
|
153
|
+
name: row.partition,
|
|
154
|
+
stats: stats
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
Object.keys(stats).forEach(key => {
|
|
158
|
+
if (queueData.totals[key] !== undefined) {
|
|
159
|
+
queueData.totals[key] += stats[key];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Calculate namespace totals
|
|
165
|
+
const totals = {
|
|
166
|
+
pending: 0,
|
|
167
|
+
processing: 0,
|
|
168
|
+
completed: 0,
|
|
169
|
+
failed: 0,
|
|
170
|
+
deadLetter: 0,
|
|
171
|
+
total: 0
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
queueMap.forEach(queueData => {
|
|
175
|
+
Object.keys(queueData.totals).forEach(key => {
|
|
176
|
+
totals[key] += queueData.totals[key];
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
namespace,
|
|
182
|
+
totals,
|
|
183
|
+
queues: Array.from(queueMap.values())
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getTaskStats = async (task, filters = {}) => {
|
|
188
|
+
const { fromDateTime, toDateTime } = filters;
|
|
189
|
+
const stats = await queueManager.getQueueStats({
|
|
190
|
+
task,
|
|
191
|
+
fromDateTime,
|
|
192
|
+
toDateTime
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Group by queue
|
|
196
|
+
const queueMap = new Map();
|
|
197
|
+
|
|
198
|
+
stats.forEach(row => {
|
|
199
|
+
if (!queueMap.has(row.queue)) {
|
|
200
|
+
queueMap.set(row.queue, {
|
|
201
|
+
queue: row.queue,
|
|
202
|
+
namespace: row.namespace,
|
|
203
|
+
partitions: [],
|
|
204
|
+
totals: {
|
|
205
|
+
pending: 0,
|
|
206
|
+
processing: 0,
|
|
207
|
+
completed: 0,
|
|
208
|
+
failed: 0,
|
|
209
|
+
deadLetter: 0,
|
|
210
|
+
total: 0
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const queueData = queueMap.get(row.queue);
|
|
216
|
+
|
|
217
|
+
// Create stats object from row data
|
|
218
|
+
const stats = {
|
|
219
|
+
pending: parseInt(row.pending || 0),
|
|
220
|
+
processing: parseInt(row.processing || 0),
|
|
221
|
+
completed: parseInt(row.completed || 0),
|
|
222
|
+
failed: parseInt(row.failed || 0),
|
|
223
|
+
deadLetter: parseInt(row.dead_letter || 0),
|
|
224
|
+
total: parseInt(row.total_messages || 0)
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
queueData.partitions.push({
|
|
228
|
+
name: row.partition,
|
|
229
|
+
stats: stats
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
Object.keys(stats).forEach(key => {
|
|
233
|
+
if (queueData.totals[key] !== undefined) {
|
|
234
|
+
queueData.totals[key] += stats[key];
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Calculate task totals
|
|
240
|
+
const totals = {
|
|
241
|
+
pending: 0,
|
|
242
|
+
processing: 0,
|
|
243
|
+
completed: 0,
|
|
244
|
+
failed: 0,
|
|
245
|
+
deadLetter: 0,
|
|
246
|
+
total: 0
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
queueMap.forEach(queueData => {
|
|
250
|
+
Object.keys(queueData.totals).forEach(key => {
|
|
251
|
+
totals[key] += queueData.totals[key];
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
task,
|
|
257
|
+
totals,
|
|
258
|
+
queues: Array.from(queueMap.values())
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const getQueueDepths = async (filters = {}) => {
|
|
263
|
+
const { fromDateTime, toDateTime, queue, namespace, task } = filters;
|
|
264
|
+
const stats = await queueManager.getQueueStats({
|
|
265
|
+
fromDateTime,
|
|
266
|
+
toDateTime,
|
|
267
|
+
queue,
|
|
268
|
+
namespace,
|
|
269
|
+
task
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Group by queue for aggregation
|
|
273
|
+
const queueMap = new Map();
|
|
274
|
+
|
|
275
|
+
stats.forEach(row => {
|
|
276
|
+
if (!queueMap.has(row.queue)) {
|
|
277
|
+
queueMap.set(row.queue, {
|
|
278
|
+
queue: row.queue,
|
|
279
|
+
depth: 0,
|
|
280
|
+
processing: 0,
|
|
281
|
+
partitions: []
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const queueData = queueMap.get(row.queue);
|
|
286
|
+
const pending = parseInt(row.pending || 0);
|
|
287
|
+
const processing = parseInt(row.processing || 0);
|
|
288
|
+
queueData.depth += pending;
|
|
289
|
+
queueData.processing += processing;
|
|
290
|
+
queueData.partitions.push({
|
|
291
|
+
name: row.partition,
|
|
292
|
+
depth: pending,
|
|
293
|
+
processing: processing
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
depths: Array.from(queueMap.values())
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const getThroughput = async (pool, filters = {}) => {
|
|
303
|
+
// Support custom time window or default to last hour
|
|
304
|
+
const { fromDateTime, toDateTime, queue, namespace, task } = filters;
|
|
305
|
+
|
|
306
|
+
// Calculate time window based on provided dates or use defaults
|
|
307
|
+
let timeWindow = '1 hour';
|
|
308
|
+
let minuteInterval = 60; // Number of minutes to fetch
|
|
309
|
+
let startTime, endTime;
|
|
310
|
+
|
|
311
|
+
if (fromDateTime && toDateTime) {
|
|
312
|
+
startTime = new Date(fromDateTime);
|
|
313
|
+
endTime = new Date(toDateTime);
|
|
314
|
+
const diffMs = endTime - startTime;
|
|
315
|
+
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|
316
|
+
minuteInterval = Math.min(diffMinutes, 1440); // Cap at 24 hours worth of minutes
|
|
317
|
+
timeWindow = `${diffMinutes} minutes`;
|
|
318
|
+
} else {
|
|
319
|
+
startTime = new Date(Date.now() - 60 * 60 * 1000); // 1 hour ago
|
|
320
|
+
endTime = new Date();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Build filter conditions for queries
|
|
324
|
+
const filterConditions = [];
|
|
325
|
+
const filterParams = [];
|
|
326
|
+
let paramCounter = 1;
|
|
327
|
+
|
|
328
|
+
if (queue) {
|
|
329
|
+
filterConditions.push(`q.name = $${paramCounter}`);
|
|
330
|
+
filterParams.push(queue);
|
|
331
|
+
paramCounter++;
|
|
332
|
+
}
|
|
333
|
+
if (namespace) {
|
|
334
|
+
filterConditions.push(`q.namespace = $${paramCounter}`);
|
|
335
|
+
filterParams.push(namespace);
|
|
336
|
+
paramCounter++;
|
|
337
|
+
}
|
|
338
|
+
if (task) {
|
|
339
|
+
filterConditions.push(`q.task = $${paramCounter}`);
|
|
340
|
+
filterParams.push(task);
|
|
341
|
+
paramCounter++;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const whereClause = filterConditions.length > 0
|
|
345
|
+
? `AND ${filterConditions.join(' AND ')}`
|
|
346
|
+
: '';
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
// 1. Incoming messages (created/inserted)
|
|
350
|
+
const incomingQuery = `
|
|
351
|
+
WITH time_series AS (
|
|
352
|
+
SELECT generate_series(
|
|
353
|
+
DATE_TRUNC('minute', $${paramCounter}::timestamp),
|
|
354
|
+
DATE_TRUNC('minute', $${paramCounter + 1}::timestamp),
|
|
355
|
+
'1 minute'::interval
|
|
356
|
+
) AS minute
|
|
357
|
+
)
|
|
358
|
+
SELECT
|
|
359
|
+
ts.minute,
|
|
360
|
+
COALESCE(COUNT(m.id), 0) as count
|
|
361
|
+
FROM time_series ts
|
|
362
|
+
LEFT JOIN queen.messages m ON
|
|
363
|
+
DATE_TRUNC('minute', m.created_at) = ts.minute
|
|
364
|
+
AND m.created_at >= $${paramCounter}::timestamp
|
|
365
|
+
AND m.created_at <= $${paramCounter + 1}::timestamp
|
|
366
|
+
${whereClause ? `LEFT JOIN queen.partitions p ON m.partition_id = p.id
|
|
367
|
+
LEFT JOIN queen.queues q ON p.queue_id = q.id` : ''}
|
|
368
|
+
${whereClause ? `WHERE 1=1 ${whereClause}` : ''}
|
|
369
|
+
GROUP BY ts.minute
|
|
370
|
+
ORDER BY ts.minute DESC
|
|
371
|
+
LIMIT ${minuteInterval}
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
// 2. Completed messages
|
|
375
|
+
const completedQuery = `
|
|
376
|
+
WITH time_series AS (
|
|
377
|
+
SELECT generate_series(
|
|
378
|
+
DATE_TRUNC('minute', $${paramCounter}::timestamp),
|
|
379
|
+
DATE_TRUNC('minute', $${paramCounter + 1}::timestamp),
|
|
380
|
+
'1 minute'::interval
|
|
381
|
+
) AS minute
|
|
382
|
+
)
|
|
383
|
+
SELECT
|
|
384
|
+
ts.minute,
|
|
385
|
+
COALESCE(COUNT(ms.id), 0) as count
|
|
386
|
+
FROM time_series ts
|
|
387
|
+
LEFT JOIN queen.messages_status ms ON
|
|
388
|
+
DATE_TRUNC('minute', ms.completed_at) = ts.minute
|
|
389
|
+
AND ms.completed_at >= $${paramCounter}::timestamp
|
|
390
|
+
AND ms.completed_at <= $${paramCounter + 1}::timestamp
|
|
391
|
+
AND ms.status = 'completed'
|
|
392
|
+
AND ms.consumer_group IS NULL
|
|
393
|
+
${whereClause ? `LEFT JOIN queen.messages m ON ms.message_id = m.id
|
|
394
|
+
LEFT JOIN queen.partitions p ON m.partition_id = p.id
|
|
395
|
+
LEFT JOIN queen.queues q ON p.queue_id = q.id` : ''}
|
|
396
|
+
${whereClause ? `WHERE 1=1 ${whereClause}` : ''}
|
|
397
|
+
GROUP BY ts.minute
|
|
398
|
+
ORDER BY ts.minute DESC
|
|
399
|
+
LIMIT ${minuteInterval}
|
|
400
|
+
`;
|
|
401
|
+
|
|
402
|
+
// 3. Processing messages (started processing in this minute)
|
|
403
|
+
const processingQuery = `
|
|
404
|
+
WITH time_series AS (
|
|
405
|
+
SELECT generate_series(
|
|
406
|
+
DATE_TRUNC('minute', $${paramCounter}::timestamp),
|
|
407
|
+
DATE_TRUNC('minute', $${paramCounter + 1}::timestamp),
|
|
408
|
+
'1 minute'::interval
|
|
409
|
+
) AS minute
|
|
410
|
+
)
|
|
411
|
+
SELECT
|
|
412
|
+
ts.minute,
|
|
413
|
+
COALESCE(COUNT(ms.id), 0) as count
|
|
414
|
+
FROM time_series ts
|
|
415
|
+
LEFT JOIN queen.messages_status ms ON
|
|
416
|
+
DATE_TRUNC('minute', ms.processing_at) = ts.minute
|
|
417
|
+
AND ms.processing_at >= $${paramCounter}::timestamp
|
|
418
|
+
AND ms.processing_at <= $${paramCounter + 1}::timestamp
|
|
419
|
+
AND ms.consumer_group IS NULL
|
|
420
|
+
${whereClause ? `LEFT JOIN queen.messages m ON ms.message_id = m.id
|
|
421
|
+
LEFT JOIN queen.partitions p ON m.partition_id = p.id
|
|
422
|
+
LEFT JOIN queen.queues q ON p.queue_id = q.id` : ''}
|
|
423
|
+
${whereClause ? `WHERE 1=1 ${whereClause}` : ''}
|
|
424
|
+
GROUP BY ts.minute
|
|
425
|
+
ORDER BY ts.minute DESC
|
|
426
|
+
LIMIT ${minuteInterval}
|
|
427
|
+
`;
|
|
428
|
+
|
|
429
|
+
// 4. Failed messages
|
|
430
|
+
const failedQuery = `
|
|
431
|
+
WITH time_series AS (
|
|
432
|
+
SELECT generate_series(
|
|
433
|
+
DATE_TRUNC('minute', $${paramCounter}::timestamp),
|
|
434
|
+
DATE_TRUNC('minute', $${paramCounter + 1}::timestamp),
|
|
435
|
+
'1 minute'::interval
|
|
436
|
+
) AS minute
|
|
437
|
+
)
|
|
438
|
+
SELECT
|
|
439
|
+
ts.minute,
|
|
440
|
+
COALESCE(COUNT(ms.id), 0) as count
|
|
441
|
+
FROM time_series ts
|
|
442
|
+
LEFT JOIN queen.messages_status ms ON
|
|
443
|
+
DATE_TRUNC('minute', ms.failed_at) = ts.minute
|
|
444
|
+
AND ms.failed_at >= $${paramCounter}::timestamp
|
|
445
|
+
AND ms.failed_at <= $${paramCounter + 1}::timestamp
|
|
446
|
+
AND ms.status = 'failed'
|
|
447
|
+
AND ms.consumer_group IS NULL
|
|
448
|
+
${whereClause ? `LEFT JOIN queen.messages m ON ms.message_id = m.id
|
|
449
|
+
LEFT JOIN queen.partitions p ON m.partition_id = p.id
|
|
450
|
+
LEFT JOIN queen.queues q ON p.queue_id = q.id` : ''}
|
|
451
|
+
${whereClause ? `WHERE 1=1 ${whereClause}` : ''}
|
|
452
|
+
GROUP BY ts.minute
|
|
453
|
+
ORDER BY ts.minute DESC
|
|
454
|
+
LIMIT ${minuteInterval}
|
|
455
|
+
`;
|
|
456
|
+
|
|
457
|
+
// 5. Dead letter messages
|
|
458
|
+
const deadLetterQuery = `
|
|
459
|
+
WITH time_series AS (
|
|
460
|
+
SELECT generate_series(
|
|
461
|
+
DATE_TRUNC('minute', $${paramCounter}::timestamp),
|
|
462
|
+
DATE_TRUNC('minute', $${paramCounter + 1}::timestamp),
|
|
463
|
+
'1 minute'::interval
|
|
464
|
+
) AS minute
|
|
465
|
+
)
|
|
466
|
+
SELECT
|
|
467
|
+
ts.minute,
|
|
468
|
+
COALESCE(COUNT(ms.id), 0) as count
|
|
469
|
+
FROM time_series ts
|
|
470
|
+
LEFT JOIN queen.messages_status ms ON
|
|
471
|
+
DATE_TRUNC('minute', ms.failed_at) = ts.minute
|
|
472
|
+
AND ms.failed_at >= $${paramCounter}::timestamp
|
|
473
|
+
AND ms.failed_at <= $${paramCounter + 1}::timestamp
|
|
474
|
+
AND ms.status = 'dead_letter'
|
|
475
|
+
AND ms.consumer_group IS NULL
|
|
476
|
+
${whereClause ? `LEFT JOIN queen.messages m ON ms.message_id = m.id
|
|
477
|
+
LEFT JOIN queen.partitions p ON m.partition_id = p.id
|
|
478
|
+
LEFT JOIN queen.queues q ON p.queue_id = q.id` : ''}
|
|
479
|
+
${whereClause ? `WHERE 1=1 ${whereClause}` : ''}
|
|
480
|
+
GROUP BY ts.minute
|
|
481
|
+
ORDER BY ts.minute DESC
|
|
482
|
+
LIMIT ${minuteInterval}
|
|
483
|
+
`;
|
|
484
|
+
|
|
485
|
+
// 6. Average lag (processing time) per minute
|
|
486
|
+
const lagQuery = `
|
|
487
|
+
WITH time_series AS (
|
|
488
|
+
SELECT generate_series(
|
|
489
|
+
DATE_TRUNC('minute', $${paramCounter}::timestamp),
|
|
490
|
+
DATE_TRUNC('minute', $${paramCounter + 1}::timestamp),
|
|
491
|
+
'1 minute'::interval
|
|
492
|
+
) AS minute
|
|
493
|
+
)
|
|
494
|
+
SELECT
|
|
495
|
+
ts.minute,
|
|
496
|
+
COALESCE(
|
|
497
|
+
AVG(
|
|
498
|
+
EXTRACT(EPOCH FROM (ms.completed_at - m.created_at))
|
|
499
|
+
),
|
|
500
|
+
0
|
|
501
|
+
) as avg_lag_seconds,
|
|
502
|
+
COUNT(m.id) as sample_count
|
|
503
|
+
FROM time_series ts
|
|
504
|
+
LEFT JOIN queen.messages_status ms ON
|
|
505
|
+
DATE_TRUNC('minute', ms.completed_at) = ts.minute
|
|
506
|
+
AND ms.completed_at >= $${paramCounter}::timestamp
|
|
507
|
+
AND ms.completed_at <= $${paramCounter + 1}::timestamp
|
|
508
|
+
AND ms.status IN ('completed', 'failed')
|
|
509
|
+
AND ms.completed_at IS NOT NULL
|
|
510
|
+
AND ms.consumer_group IS NULL
|
|
511
|
+
LEFT JOIN queen.messages m ON ms.message_id = m.id
|
|
512
|
+
AND m.created_at IS NOT NULL
|
|
513
|
+
${whereClause ? `LEFT JOIN queen.partitions p ON m.partition_id = p.id
|
|
514
|
+
LEFT JOIN queen.queues q ON p.queue_id = q.id` : ''}
|
|
515
|
+
${whereClause ? `WHERE 1=1 ${whereClause}` : ''}
|
|
516
|
+
GROUP BY ts.minute
|
|
517
|
+
ORDER BY ts.minute DESC
|
|
518
|
+
LIMIT ${minuteInterval}
|
|
519
|
+
`;
|
|
520
|
+
|
|
521
|
+
// Prepare query parameters
|
|
522
|
+
const queryParams = [...filterParams, startTime, endTime];
|
|
523
|
+
|
|
524
|
+
// Execute all queries in parallel
|
|
525
|
+
const [incoming, completed, processing, failed, deadLetter, lag] = await Promise.all([
|
|
526
|
+
pool.query(incomingQuery, queryParams),
|
|
527
|
+
pool.query(completedQuery, queryParams),
|
|
528
|
+
pool.query(processingQuery, queryParams),
|
|
529
|
+
pool.query(failedQuery, queryParams),
|
|
530
|
+
pool.query(deadLetterQuery, queryParams),
|
|
531
|
+
pool.query(lagQuery, queryParams)
|
|
532
|
+
]);
|
|
533
|
+
|
|
534
|
+
// If we have no data at all in the last hour, get the last 10 minutes of actual data
|
|
535
|
+
if (incoming.rows.every(row => row.count === 0 || row.count === '0')) {
|
|
536
|
+
const fallbackQuery = `
|
|
537
|
+
SELECT
|
|
538
|
+
DATE_TRUNC('minute', m.created_at) as minute,
|
|
539
|
+
COUNT(DISTINCT m.id) as incoming_count,
|
|
540
|
+
COUNT(DISTINCT CASE WHEN ms.status = 'completed' THEN m.id END) as completed_count,
|
|
541
|
+
COUNT(DISTINCT CASE WHEN ms.status = 'processing' THEN m.id END) as processing_count,
|
|
542
|
+
COUNT(DISTINCT CASE WHEN ms.status = 'failed' THEN m.id END) as failed_count,
|
|
543
|
+
COUNT(DISTINCT CASE WHEN ms.status = 'dead_letter' THEN m.id END) as dead_letter_count,
|
|
544
|
+
AVG(
|
|
545
|
+
CASE
|
|
546
|
+
WHEN ms.completed_at IS NOT NULL AND m.created_at IS NOT NULL
|
|
547
|
+
THEN EXTRACT(EPOCH FROM (ms.completed_at - m.created_at))
|
|
548
|
+
ELSE NULL
|
|
549
|
+
END
|
|
550
|
+
) as avg_lag_seconds
|
|
551
|
+
FROM queen.messages m
|
|
552
|
+
LEFT JOIN queen.messages_status ms ON m.id = ms.message_id AND ms.consumer_group IS NULL
|
|
553
|
+
WHERE m.created_at IS NOT NULL
|
|
554
|
+
GROUP BY minute
|
|
555
|
+
ORDER BY minute DESC
|
|
556
|
+
LIMIT 10
|
|
557
|
+
`;
|
|
558
|
+
|
|
559
|
+
const fallbackResult = await pool.query(fallbackQuery);
|
|
560
|
+
|
|
561
|
+
const throughput = fallbackResult.rows.map(row => ({
|
|
562
|
+
timestamp: row.minute,
|
|
563
|
+
incoming: {
|
|
564
|
+
messagesPerMinute: parseInt(row.incoming_count || 0),
|
|
565
|
+
messagesPerSecond: Math.round(parseInt(row.incoming_count || 0) / 60)
|
|
566
|
+
},
|
|
567
|
+
completed: {
|
|
568
|
+
messagesPerMinute: parseInt(row.completed_count || 0),
|
|
569
|
+
messagesPerSecond: Math.round(parseInt(row.completed_count || 0) / 60)
|
|
570
|
+
},
|
|
571
|
+
processing: {
|
|
572
|
+
messagesPerMinute: parseInt(row.processing_count || 0),
|
|
573
|
+
messagesPerSecond: Math.round(parseInt(row.processing_count || 0) / 60)
|
|
574
|
+
},
|
|
575
|
+
failed: {
|
|
576
|
+
messagesPerMinute: parseInt(row.failed_count || 0),
|
|
577
|
+
messagesPerSecond: Math.round(parseInt(row.failed_count || 0) / 60)
|
|
578
|
+
},
|
|
579
|
+
deadLetter: {
|
|
580
|
+
messagesPerMinute: parseInt(row.dead_letter_count || 0),
|
|
581
|
+
messagesPerSecond: Math.round(parseInt(row.dead_letter_count || 0) / 60)
|
|
582
|
+
},
|
|
583
|
+
lag: {
|
|
584
|
+
avgSeconds: parseFloat(row.avg_lag_seconds || 0),
|
|
585
|
+
avgMilliseconds: Math.round(parseFloat(row.avg_lag_seconds || 0) * 1000)
|
|
586
|
+
}
|
|
587
|
+
}));
|
|
588
|
+
|
|
589
|
+
return { throughput };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Combine all metrics by timestamp
|
|
593
|
+
const throughputMap = new Map();
|
|
594
|
+
|
|
595
|
+
// Initialize with time series
|
|
596
|
+
incoming.rows.forEach(row => {
|
|
597
|
+
throughputMap.set(row.minute.toISOString(), {
|
|
598
|
+
timestamp: row.minute,
|
|
599
|
+
incoming: {
|
|
600
|
+
messagesPerMinute: parseInt(row.count || 0),
|
|
601
|
+
messagesPerSecond: Math.round(parseInt(row.count || 0) / 60)
|
|
602
|
+
},
|
|
603
|
+
completed: {
|
|
604
|
+
messagesPerMinute: 0,
|
|
605
|
+
messagesPerSecond: 0
|
|
606
|
+
},
|
|
607
|
+
processing: {
|
|
608
|
+
messagesPerMinute: 0,
|
|
609
|
+
messagesPerSecond: 0
|
|
610
|
+
},
|
|
611
|
+
failed: {
|
|
612
|
+
messagesPerMinute: 0,
|
|
613
|
+
messagesPerSecond: 0
|
|
614
|
+
},
|
|
615
|
+
deadLetter: {
|
|
616
|
+
messagesPerMinute: 0,
|
|
617
|
+
messagesPerSecond: 0
|
|
618
|
+
},
|
|
619
|
+
lag: {
|
|
620
|
+
avgSeconds: 0,
|
|
621
|
+
avgMilliseconds: 0
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Add completed metrics
|
|
627
|
+
completed.rows.forEach(row => {
|
|
628
|
+
const key = row.minute.toISOString();
|
|
629
|
+
if (throughputMap.has(key)) {
|
|
630
|
+
const entry = throughputMap.get(key);
|
|
631
|
+
entry.completed = {
|
|
632
|
+
messagesPerMinute: parseInt(row.count || 0),
|
|
633
|
+
messagesPerSecond: Math.round(parseInt(row.count || 0) / 60)
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Add processing metrics
|
|
639
|
+
processing.rows.forEach(row => {
|
|
640
|
+
const key = row.minute.toISOString();
|
|
641
|
+
if (throughputMap.has(key)) {
|
|
642
|
+
const entry = throughputMap.get(key);
|
|
643
|
+
entry.processing = {
|
|
644
|
+
messagesPerMinute: parseInt(row.count || 0),
|
|
645
|
+
messagesPerSecond: Math.round(parseInt(row.count || 0) / 60)
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Add failed metrics
|
|
651
|
+
failed.rows.forEach(row => {
|
|
652
|
+
const key = row.minute.toISOString();
|
|
653
|
+
if (throughputMap.has(key)) {
|
|
654
|
+
const entry = throughputMap.get(key);
|
|
655
|
+
entry.failed = {
|
|
656
|
+
messagesPerMinute: parseInt(row.count || 0),
|
|
657
|
+
messagesPerSecond: Math.round(parseInt(row.count || 0) / 60)
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// Add dead letter metrics
|
|
663
|
+
deadLetter.rows.forEach(row => {
|
|
664
|
+
const key = row.minute.toISOString();
|
|
665
|
+
if (throughputMap.has(key)) {
|
|
666
|
+
const entry = throughputMap.get(key);
|
|
667
|
+
entry.deadLetter = {
|
|
668
|
+
messagesPerMinute: parseInt(row.count || 0),
|
|
669
|
+
messagesPerSecond: Math.round(parseInt(row.count || 0) / 60)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Add lag metrics
|
|
675
|
+
lag.rows.forEach(row => {
|
|
676
|
+
const key = row.minute.toISOString();
|
|
677
|
+
if (throughputMap.has(key)) {
|
|
678
|
+
const entry = throughputMap.get(key);
|
|
679
|
+
const avgSeconds = parseFloat(row.avg_lag_seconds || 0);
|
|
680
|
+
entry.lag = {
|
|
681
|
+
avgSeconds: avgSeconds,
|
|
682
|
+
avgMilliseconds: Math.round(avgSeconds * 1000),
|
|
683
|
+
sampleCount: parseInt(row.sample_count || 0)
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// Convert map to array and sort by timestamp (newest first)
|
|
689
|
+
const throughput = Array.from(throughputMap.values())
|
|
690
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
691
|
+
|
|
692
|
+
return { throughput };
|
|
693
|
+
|
|
694
|
+
} catch (error) {
|
|
695
|
+
console.error('Error calculating throughput metrics:', error);
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const getQueueLag = async (filters = {}) => {
|
|
701
|
+
const { fromDateTime, toDateTime, queue, namespace, task } = filters;
|
|
702
|
+
const lagStats = await queueManager.getQueueLag({
|
|
703
|
+
fromDateTime,
|
|
704
|
+
toDateTime,
|
|
705
|
+
queue,
|
|
706
|
+
namespace,
|
|
707
|
+
task
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Group by queue for aggregation
|
|
711
|
+
const queueMap = new Map();
|
|
712
|
+
|
|
713
|
+
lagStats.forEach(row => {
|
|
714
|
+
if (!queueMap.has(row.queue)) {
|
|
715
|
+
queueMap.set(row.queue, {
|
|
716
|
+
queue: row.queue,
|
|
717
|
+
namespace: row.namespace,
|
|
718
|
+
task: row.task,
|
|
719
|
+
partitions: [],
|
|
720
|
+
totals: {
|
|
721
|
+
pendingCount: 0,
|
|
722
|
+
processingCount: 0,
|
|
723
|
+
totalBacklog: 0,
|
|
724
|
+
completedMessages: 0,
|
|
725
|
+
avgProcessingTimeSeconds: 0,
|
|
726
|
+
medianProcessingTimeSeconds: 0,
|
|
727
|
+
p95ProcessingTimeSeconds: 0,
|
|
728
|
+
estimatedLagSeconds: 0,
|
|
729
|
+
medianLagSeconds: 0,
|
|
730
|
+
p95LagSeconds: 0
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const queueData = queueMap.get(row.queue);
|
|
736
|
+
|
|
737
|
+
// Handle case where stats might not exist or be malformed
|
|
738
|
+
const stats = row.stats || {
|
|
739
|
+
pendingCount: 0,
|
|
740
|
+
processingCount: 0,
|
|
741
|
+
totalBacklog: 0,
|
|
742
|
+
completedMessages: 0,
|
|
743
|
+
avgProcessingTimeSeconds: 0,
|
|
744
|
+
medianProcessingTimeSeconds: 0,
|
|
745
|
+
p95ProcessingTimeSeconds: 0,
|
|
746
|
+
estimatedLagSeconds: 0,
|
|
747
|
+
medianLagSeconds: 0,
|
|
748
|
+
p95LagSeconds: 0
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
queueData.partitions.push({
|
|
752
|
+
name: row.partition,
|
|
753
|
+
stats: stats
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Aggregate totals (weighted averages for processing times)
|
|
757
|
+
const currentTotal = queueData.totals.completedMessages;
|
|
758
|
+
const newTotal = currentTotal + (stats.completedMessages || 0);
|
|
759
|
+
|
|
760
|
+
if (newTotal > 0) {
|
|
761
|
+
// Weighted average for processing times
|
|
762
|
+
queueData.totals.avgProcessingTimeSeconds =
|
|
763
|
+
(queueData.totals.avgProcessingTimeSeconds * currentTotal +
|
|
764
|
+
(stats.avgProcessingTimeSeconds || 0) * (stats.completedMessages || 0)) / newTotal;
|
|
765
|
+
queueData.totals.medianProcessingTimeSeconds =
|
|
766
|
+
(queueData.totals.medianProcessingTimeSeconds * currentTotal +
|
|
767
|
+
(stats.medianProcessingTimeSeconds || 0) * (stats.completedMessages || 0)) / newTotal;
|
|
768
|
+
queueData.totals.p95ProcessingTimeSeconds =
|
|
769
|
+
Math.max(queueData.totals.p95ProcessingTimeSeconds, stats.p95ProcessingTimeSeconds || 0);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Sum up counts and lags
|
|
773
|
+
queueData.totals.pendingCount += stats.pendingCount || 0;
|
|
774
|
+
queueData.totals.processingCount += stats.processingCount || 0;
|
|
775
|
+
queueData.totals.totalBacklog += stats.totalBacklog || 0;
|
|
776
|
+
queueData.totals.completedMessages = newTotal;
|
|
777
|
+
queueData.totals.estimatedLagSeconds += stats.estimatedLagSeconds || 0;
|
|
778
|
+
queueData.totals.medianLagSeconds += stats.medianLagSeconds || 0;
|
|
779
|
+
queueData.totals.p95LagSeconds += stats.p95LagSeconds || 0;
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Add human-readable formats to totals
|
|
783
|
+
Array.from(queueMap.values()).forEach(queueData => {
|
|
784
|
+
const formatDuration = (seconds) => {
|
|
785
|
+
if (seconds === 0) return '0s';
|
|
786
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
787
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;
|
|
788
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
789
|
+
return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
queueData.totals.estimatedLag = formatDuration(queueData.totals.estimatedLagSeconds);
|
|
793
|
+
queueData.totals.medianLag = formatDuration(queueData.totals.medianLagSeconds);
|
|
794
|
+
queueData.totals.p95Lag = formatDuration(queueData.totals.p95LagSeconds);
|
|
795
|
+
queueData.totals.avgProcessingTime = formatDuration(queueData.totals.avgProcessingTimeSeconds);
|
|
796
|
+
queueData.totals.medianProcessingTime = formatDuration(queueData.totals.medianProcessingTimeSeconds);
|
|
797
|
+
queueData.totals.p95ProcessingTime = formatDuration(queueData.totals.p95ProcessingTimeSeconds);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
return { queues: Array.from(queueMap.values()) };
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
getQueues,
|
|
805
|
+
getQueueStats,
|
|
806
|
+
getNamespaceStats,
|
|
807
|
+
getTaskStats,
|
|
808
|
+
getQueueDepths,
|
|
809
|
+
getThroughput,
|
|
810
|
+
getQueueLag
|
|
811
|
+
};
|
|
812
|
+
};
|