queen-mq 0.1.0 → 0.1.2

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.
Files changed (160) hide show
  1. package/API.md +862 -752
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +1705 -1051
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-complete-client.js +260 -0
  17. package/examples/test-dashboard-api.js +200 -0
  18. package/examples/test-traceid.js +147 -0
  19. package/package.json +17 -4
  20. package/server.log +1 -0
  21. package/src/benchmark/consumer.js +207 -0
  22. package/src/benchmark/consumer_multi.js +216 -0
  23. package/src/benchmark/producer.js +75 -0
  24. package/src/benchmark/producer_multi.js +115 -0
  25. package/src/client/client.js +300 -31
  26. package/src/client/queenClient.js +5 -0
  27. package/src/cluster-server.js +242 -0
  28. package/src/config.js +19 -5
  29. package/src/database/connection.js +42 -16
  30. package/src/database/poolManager.js +7 -0
  31. package/src/database/schema-v2.sql +194 -130
  32. package/src/managers/queueManagerOptimized.js +823 -933
  33. package/src/managers/systemEventManager.js +8 -3
  34. package/src/routes/messages.js +127 -57
  35. package/src/routes/pop.js +27 -43
  36. package/src/routes/resources.js +61 -27
  37. package/src/routes/status.js +1037 -0
  38. package/src/server.js +308 -272
  39. package/src/services/evictionService.js +57 -28
  40. package/src/services/retentionService.js +44 -11
  41. package/src/test/MIGRATION_ISSUES.md +174 -0
  42. package/src/test/README.md +203 -0
  43. package/src/test/advanced-pattern-tests.js +1137 -0
  44. package/src/test/bus-mode-tests.js +361 -0
  45. package/src/test/core-tests.js +342 -0
  46. package/src/test/edge-case-tests.js +561 -0
  47. package/src/test/enterprise-tests.js +637 -0
  48. package/src/test/partition-locking-tests.js +545 -0
  49. package/src/test/test-new.js +278 -0
  50. package/src/test/test.js +6 -3
  51. package/src/test/utils.js +169 -0
  52. package/src/utils/streaming.js +231 -0
  53. package/src/utils/uuid.js +2 -2
  54. package/src/websocket/wsServer.js +10 -3
  55. package/test-keepalive-v2.sh +22 -0
  56. package/webapp/COLOR_GUIDE.md +118 -0
  57. package/webapp/README.md +143 -0
  58. package/webapp/index.html +14 -0
  59. package/webapp/package-lock.json +3184 -0
  60. package/webapp/package.json +25 -0
  61. package/webapp/postcss.config.js +7 -0
  62. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  63. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  64. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  65. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  66. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  67. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  68. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  69. package/webapp/public/assets/queen-logo.svg +263 -0
  70. package/webapp/src/App.vue +19 -0
  71. package/webapp/src/api/analytics.js +10 -0
  72. package/webapp/src/api/client.js +29 -0
  73. package/webapp/src/api/consumers.js +52 -0
  74. package/webapp/src/api/health.js +7 -0
  75. package/webapp/src/api/messages.js +26 -0
  76. package/webapp/src/api/queues.js +14 -0
  77. package/webapp/src/api/resources.js +8 -0
  78. package/webapp/src/assets/styles/main.css +357 -0
  79. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  80. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  81. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  82. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  83. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  84. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  85. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  86. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  87. package/webapp/src/components/common/MetricCard.vue +43 -0
  88. package/webapp/src/components/common/StatusBadge.vue +45 -0
  89. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  90. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  91. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  92. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  93. package/webapp/src/components/layout/AppLayout.vue +110 -0
  94. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  95. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  96. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  97. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  98. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  99. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  100. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  101. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  102. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  103. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  104. package/webapp/src/composables/useApi.js +34 -0
  105. package/webapp/src/composables/useTheme.js +36 -0
  106. package/webapp/src/main.js +11 -0
  107. package/webapp/src/router/index.js +42 -0
  108. package/webapp/src/utils/colors.js +96 -0
  109. package/webapp/src/utils/formatters.js +49 -0
  110. package/webapp/src/views/Analytics.vue +377 -0
  111. package/webapp/src/views/ConsumerGroups.vue +433 -0
  112. package/webapp/src/views/Dashboard.vue +418 -0
  113. package/webapp/src/views/Messages.vue +361 -0
  114. package/webapp/src/views/QueueDetail.vue +582 -0
  115. package/webapp/src/views/Queues.vue +496 -0
  116. package/webapp/tailwind.config.js +25 -0
  117. package/webapp/vite.config.js +10 -0
  118. package/CACHE.md +0 -519
  119. package/DASHBOARD-V3.md +0 -478
  120. package/DASHBOARD.md +0 -382
  121. package/MOD_QUEUE.md +0 -453
  122. package/PARTITION_LOCKING_DESIGN.md +0 -989
  123. package/PLAN.md +0 -707
  124. package/QUERY_ANALSYS.md +0 -72
  125. package/QUEUE_BUS.md +0 -334
  126. package/V2-PLAN.md +0 -236
  127. package/dashboard/.vscode/extensions.json +0 -3
  128. package/dashboard/README.md +0 -5
  129. package/dashboard/index.html +0 -14
  130. package/dashboard/package-lock.json +0 -1458
  131. package/dashboard/package.json +0 -25
  132. package/dashboard/public/vite.svg +0 -1
  133. package/dashboard/src/App.vue +0 -29
  134. package/dashboard/src/assets/styles/main.css +0 -908
  135. package/dashboard/src/assets/vue.svg +0 -1
  136. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  137. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  138. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  139. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  140. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  141. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  142. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  143. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  144. package/dashboard/src/main.js +0 -44
  145. package/dashboard/src/router.js +0 -54
  146. package/dashboard/src/services/api.js +0 -187
  147. package/dashboard/src/services/websocket.js +0 -167
  148. package/dashboard/src/utils/constants.js +0 -56
  149. package/dashboard/src/utils/helpers.js +0 -118
  150. package/dashboard/src/views/Analytics.vue +0 -912
  151. package/dashboard/src/views/Dashboard.vue +0 -906
  152. package/dashboard/src/views/Messages.vue +0 -437
  153. package/dashboard/src/views/QueueDetail.vue +0 -501
  154. package/dashboard/src/views/Queues.vue +0 -333
  155. package/dashboard/vite.config.js +0 -30
  156. package/debug-namespace.js +0 -110
  157. package/docs/long-polling.md +0 -159
  158. package/docs/multi-server-cache-solutions.md +0 -185
  159. package/docs/performance-tuning.md +0 -222
  160. package/src/routes/analytics.js +0 -812
@@ -1,812 +0,0 @@
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
- };