queen-mq 0.2.0 → 0.2.22

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 (85) hide show
  1. package/README.md +186 -763
  2. package/{src → client-js}/benchmark/consumer.js +6 -4
  3. package/{src → client-js}/benchmark/producer.js +9 -4
  4. package/{src → client-js}/client/client.js +277 -7
  5. package/{src → client-js}/client/index.js +1 -4
  6. package/{src → client-js}/client/utils/http.js +3 -2
  7. package/{src → client-js}/client/utils/retry.js +7 -1
  8. package/{src → client-js}/test/README.md +30 -9
  9. package/{src → client-js}/test/core-tests.js +7 -2
  10. package/{src → client-js}/test/edge-case-tests.js +2 -1
  11. package/client-js/test/human.js +162 -0
  12. package/client-js/test/partition-transaction-tests.js +482 -0
  13. package/client-js/test/qos0-tests.js +334 -0
  14. package/{src → client-js}/test/test-new.js +55 -0
  15. package/package.json +8 -17
  16. package/init-db.js +0 -20
  17. package/src/cluster-server.js +0 -242
  18. package/src/config.js +0 -229
  19. package/src/database/connection.js +0 -129
  20. package/src/database/poolManager.js +0 -199
  21. package/src/database/schema-v2.sql +0 -278
  22. package/src/managers/eventManager.js +0 -59
  23. package/src/managers/queueManagerOptimized.js +0 -1634
  24. package/src/managers/resourceCache.js +0 -96
  25. package/src/managers/systemEventManager.js +0 -132
  26. package/src/routes/ack.js +0 -26
  27. package/src/routes/configure.js +0 -46
  28. package/src/routes/messages.js +0 -368
  29. package/src/routes/pop.js +0 -69
  30. package/src/routes/push.js +0 -28
  31. package/src/routes/resources.js +0 -330
  32. package/src/routes/status.js +0 -1037
  33. package/src/server.js +0 -1633
  34. package/src/test/MIGRATION_ISSUES.md +0 -174
  35. package/src/test/test.js +0 -4524
  36. package/src/utils/streaming.js +0 -231
  37. package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
  38. package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
  39. package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
  40. package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
  41. package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
  42. package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
  43. package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
  44. package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
  45. package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
  46. package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
  47. package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
  48. package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
  49. package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
  50. package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
  51. package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
  52. package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
  53. package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
  54. package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
  55. package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
  56. package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
  57. package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
  58. package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
  59. package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
  60. package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
  61. package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
  62. package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
  63. package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
  64. package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
  65. package/src/webapp-dist/assets/queen-logo.svg +0 -263
  66. package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
  67. package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
  68. package/src/webapp-dist/index.html +0 -15
  69. package/src/websocket/wsServer.js +0 -228
  70. /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
  71. /package/{src → client-js}/benchmark/producer_multi.js +0 -0
  72. /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
  73. /package/{src → client-js}/services/encryptionService.js +0 -0
  74. /package/{src → client-js}/services/evictionService.js +0 -0
  75. /package/{src → client-js}/services/retentionService.js +0 -0
  76. /package/{src → client-js}/services/startupSync.js +0 -0
  77. /package/{src → client-js}/test/advanced-client-tests.js +0 -0
  78. /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
  79. /package/{src → client-js}/test/bus-mode-tests.js +0 -0
  80. /package/{src → client-js}/test/enterprise-tests.js +0 -0
  81. /package/{src → client-js}/test/partition-locking-tests.js +0 -0
  82. /package/{src → client-js}/test/utils.js +0 -0
  83. /package/{src → client-js}/test/window-buffer-test.js +0 -0
  84. /package/{src → client-js}/utils/logger.js +0 -0
  85. /package/{src → client-js}/utils/uuid.js +0 -0
@@ -1,1037 +0,0 @@
1
- export const createStatusRoutes = (pool) => {
2
-
3
- // Helper to format duration in human-readable format
4
- const formatDuration = (seconds) => {
5
- if (!seconds || seconds === 0) return '0s';
6
- if (seconds < 60) return `${seconds.toFixed(1)}s`;
7
- if (seconds < 3600) {
8
- const mins = Math.floor(seconds / 60);
9
- const secs = Math.floor(seconds % 60);
10
- return `${mins}m ${secs}s`;
11
- }
12
- if (seconds < 86400) {
13
- const hours = Math.floor(seconds / 3600);
14
- const mins = Math.floor((seconds % 3600) / 60);
15
- return `${hours}h ${mins}m`;
16
- }
17
- const days = Math.floor(seconds / 86400);
18
- const hours = Math.floor((seconds % 86400) / 3600);
19
- return `${days}d ${hours}h`;
20
- };
21
-
22
- // Helper to get default time range (last hour)
23
- const getTimeRange = (from, to) => {
24
- const toDate = to ? new Date(to) : new Date();
25
- const fromDate = from ? new Date(from) : new Date(toDate.getTime() - 60 * 60 * 1000);
26
- return { from: fromDate, to: toDate };
27
- };
28
-
29
- // 1. Dashboard Status Endpoint
30
- const getStatus = async (filters = {}) => {
31
- const { from, to, queue, namespace, task } = filters;
32
- const timeRange = getTimeRange(from, to);
33
-
34
- // Build filter conditions
35
- const buildFilters = (startParam = 3) => {
36
- const conditions = [];
37
- const params = [timeRange.from, timeRange.to];
38
- let paramCount = startParam;
39
-
40
- if (queue) {
41
- conditions.push(`q.name = $${paramCount}`);
42
- params.push(queue);
43
- paramCount++;
44
- }
45
- if (namespace) {
46
- conditions.push(`q.namespace = $${paramCount}`);
47
- params.push(namespace);
48
- paramCount++;
49
- }
50
- if (task) {
51
- conditions.push(`q.task = $${paramCount}`);
52
- params.push(task);
53
- paramCount++;
54
- }
55
-
56
- return { conditions, params, whereClause: conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : '' };
57
- };
58
-
59
- try {
60
- // Query 1: Throughput per minute
61
- const filterInfo = buildFilters(3);
62
- const throughputQuery = `
63
- WITH time_series AS (
64
- SELECT generate_series(
65
- DATE_TRUNC('minute', $1::timestamptz),
66
- DATE_TRUNC('minute', $2::timestamptz),
67
- '1 minute'::interval
68
- ) AS minute
69
- ),
70
- ingested_per_minute AS (
71
- SELECT
72
- DATE_TRUNC('minute', m.created_at) as minute,
73
- COUNT(DISTINCT m.id) as messages_ingested
74
- FROM queen.messages m
75
- JOIN queen.partitions p ON p.id = m.partition_id
76
- JOIN queen.queues q ON q.id = p.queue_id
77
- WHERE m.created_at >= $1::timestamptz
78
- AND m.created_at <= $2::timestamptz
79
- ${filterInfo.whereClause}
80
- GROUP BY DATE_TRUNC('minute', m.created_at)
81
- ),
82
- processed_per_minute AS (
83
- SELECT
84
- DATE_TRUNC('minute', mc.acked_at) as minute,
85
- SUM(mc.messages_completed) as messages_processed
86
- FROM queen.messages_consumed mc
87
- JOIN queen.partitions p ON p.id = mc.partition_id
88
- JOIN queen.queues q ON q.id = p.queue_id
89
- WHERE mc.acked_at >= $1::timestamptz
90
- AND mc.acked_at <= $2::timestamptz
91
- ${filterInfo.whereClause}
92
- GROUP BY DATE_TRUNC('minute', mc.acked_at)
93
- )
94
- SELECT
95
- ts.minute,
96
- COALESCE(ipm.messages_ingested, 0) as messages_ingested,
97
- COALESCE(ppm.messages_processed, 0) as messages_processed
98
- FROM time_series ts
99
- LEFT JOIN ingested_per_minute ipm ON ipm.minute = ts.minute
100
- LEFT JOIN processed_per_minute ppm ON ppm.minute = ts.minute
101
- ORDER BY ts.minute DESC
102
- `;
103
-
104
- // Query 2: Active queues in time range
105
- const queuesQuery = `
106
- SELECT
107
- q.id, q.name, q.namespace, q.task,
108
- COUNT(DISTINCT p.id) as partition_count,
109
- COALESCE(SUM(pc.total_messages_consumed), 0) as total_consumed
110
- FROM queen.queues q
111
- LEFT JOIN queen.partitions p ON p.queue_id = q.id
112
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
113
- AND pc.consumer_group = '__QUEUE_MODE__'
114
- LEFT JOIN queen.messages m ON m.partition_id = p.id
115
- AND m.created_at >= $1 AND m.created_at <= $2
116
- WHERE 1=1 ${filterInfo.whereClause}
117
- GROUP BY q.id, q.name, q.namespace, q.task
118
- HAVING COUNT(DISTINCT m.id) > 0
119
- ORDER BY q.name
120
- `;
121
-
122
- // Query 3: Message counts (using partition_consumers estimates)
123
- const countsQuery = `
124
- SELECT
125
- COUNT(DISTINCT m.id) as total_messages,
126
- (SELECT COALESCE(SUM(pending_estimate), 0)::integer
127
- FROM queen.partition_consumers
128
- WHERE consumer_group = '__QUEUE_MODE__') as pending,
129
- (SELECT COUNT(*)
130
- FROM queen.partition_consumers
131
- WHERE consumer_group = '__QUEUE_MODE__'
132
- AND lease_expires_at IS NOT NULL
133
- AND lease_expires_at > NOW()) as processing,
134
- (SELECT COALESCE(SUM(total_messages_consumed), 0)::integer
135
- FROM queen.partition_consumers
136
- WHERE consumer_group = '__QUEUE_MODE__') as completed,
137
- 0 as failed,
138
- (SELECT COUNT(*)
139
- FROM queen.dead_letter_queue
140
- WHERE consumer_group IS NULL OR consumer_group = '__QUEUE_MODE__') as dead_letter
141
- FROM queen.messages m
142
- LEFT JOIN queen.partitions p ON p.id = m.partition_id
143
- LEFT JOIN queen.queues q ON q.id = p.queue_id
144
- WHERE m.created_at >= $1 AND m.created_at <= $2
145
- ${filterInfo.whereClause}
146
- `;
147
-
148
- // Query 4: Active leases
149
- const leasesQuery = `
150
- SELECT
151
- COUNT(*) as active_leases,
152
- COUNT(DISTINCT partition_id) as partitions_with_leases,
153
- SUM(batch_size) as total_batch_size,
154
- SUM(acked_count) as total_acked
155
- FROM queen.partition_consumers
156
- WHERE lease_expires_at IS NOT NULL AND lease_expires_at > NOW()
157
- `;
158
-
159
- // Query 5: DLQ stats
160
- const dlqQuery = `
161
- SELECT
162
- COUNT(*) as total_dlq_messages,
163
- COUNT(DISTINCT partition_id) as affected_partitions,
164
- error_message,
165
- COUNT(*) as error_count
166
- FROM queen.dead_letter_queue dlq
167
- LEFT JOIN queen.partitions p ON p.id = dlq.partition_id
168
- LEFT JOIN queen.queues q ON q.id = p.queue_id
169
- WHERE dlq.failed_at >= $1 AND dlq.failed_at <= $2
170
- ${filterInfo.whereClause}
171
- GROUP BY error_message
172
- ORDER BY error_count DESC
173
- LIMIT 5
174
- `;
175
-
176
- // Execute all queries in parallel
177
- const [throughputResult, queuesResult, countsResult, leasesResult, dlqResult] = await Promise.all([
178
- pool.query(throughputQuery, filterInfo.params),
179
- pool.query(queuesQuery, filterInfo.params),
180
- pool.query(countsQuery, filterInfo.params),
181
- pool.query(leasesQuery),
182
- pool.query(dlqQuery, filterInfo.params)
183
- ]);
184
-
185
- // Format throughput data
186
- const throughput = throughputResult.rows.map(row => ({
187
- timestamp: row.minute,
188
- ingested: parseInt(row.messages_ingested),
189
- processed: parseInt(row.messages_processed),
190
- ingestedPerSecond: parseFloat((parseInt(row.messages_ingested) / 60).toFixed(2)),
191
- processedPerSecond: parseFloat((parseInt(row.messages_processed) / 60).toFixed(2))
192
- }));
193
-
194
- // Format queues data
195
- const queues = queuesResult.rows.map(row => ({
196
- id: row.id,
197
- name: row.name,
198
- namespace: row.namespace,
199
- task: row.task,
200
- partitions: parseInt(row.partition_count),
201
- totalConsumed: parseInt(row.total_consumed || 0)
202
- }));
203
-
204
- // Format message counts
205
- const counts = countsResult.rows[0] || {};
206
- const messages = {
207
- total: parseInt(counts.total_messages || 0),
208
- pending: parseInt(counts.pending || 0),
209
- processing: parseInt(counts.processing || 0),
210
- completed: parseInt(counts.completed || 0),
211
- failed: parseInt(counts.failed || 0),
212
- deadLetter: parseInt(counts.dead_letter || 0)
213
- };
214
-
215
- // Format leases data
216
- const leaseData = leasesResult.rows[0] || {};
217
- const leases = {
218
- active: parseInt(leaseData.active_leases || 0),
219
- partitionsWithLeases: parseInt(leaseData.partitions_with_leases || 0),
220
- totalBatchSize: parseInt(leaseData.total_batch_size || 0),
221
- totalAcked: parseInt(leaseData.total_acked || 0)
222
- };
223
-
224
- // Get DLQ totals and top errors
225
- const dlqTotal = dlqResult.rows.reduce((sum, row) => sum + parseInt(row.error_count || 0), 0);
226
- const dlqPartitions = new Set(dlqResult.rows.map(row => row.partition_id)).size;
227
- const topErrors = dlqResult.rows
228
- .filter(row => row.error_message)
229
- .map(row => ({
230
- error: row.error_message,
231
- count: parseInt(row.error_count)
232
- }));
233
-
234
- return {
235
- timeRange: {
236
- from: timeRange.from.toISOString(),
237
- to: timeRange.to.toISOString()
238
- },
239
- throughput,
240
- queues,
241
- messages,
242
- leases,
243
- deadLetterQueue: {
244
- totalMessages: dlqTotal,
245
- affectedPartitions: dlqPartitions,
246
- topErrors
247
- }
248
- };
249
-
250
- } catch (error) {
251
- console.error('Error in getStatus:', error);
252
- throw error;
253
- }
254
- };
255
-
256
- // 2. Queues List Endpoint
257
- const getQueues = async (filters = {}) => {
258
- const { from, to, namespace, task, limit = 100, offset = 0 } = filters;
259
- const timeRange = getTimeRange(from, to);
260
-
261
- const conditions = [];
262
- const params = [];
263
- let paramCount = 1;
264
-
265
- if (namespace) {
266
- conditions.push(`q.namespace = $${paramCount}`);
267
- params.push(namespace);
268
- paramCount++;
269
- }
270
- if (task) {
271
- conditions.push(`q.task = $${paramCount}`);
272
- params.push(task);
273
- paramCount++;
274
- }
275
-
276
- params.push(limit, offset);
277
- const whereClause = conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : '';
278
-
279
- const query = `
280
- SELECT
281
- q.id,
282
- q.name,
283
- q.namespace,
284
- q.task,
285
- q.priority,
286
- q.created_at,
287
- COUNT(DISTINCT p.id) as partition_count,
288
- (SELECT COUNT(DISTINCT m2.id)
289
- FROM queen.messages m2
290
- JOIN queen.partitions p2 ON p2.id = m2.partition_id
291
- WHERE p2.queue_id = q.id
292
- ) as total_messages,
293
- COUNT(DISTINCT CASE WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN p.id END) as processing,
294
- COALESCE(SUM(DISTINCT pc.total_messages_consumed), 0) as completed,
295
- 0 as failed,
296
- (SELECT COUNT(*) FROM queen.dead_letter_queue dlq
297
- WHERE dlq.consumer_group = '__QUEUE_MODE__') as dead_letter,
298
- (SELECT EXTRACT(EPOCH FROM (NOW() - MIN(m2.created_at)))
299
- FROM queen.messages m2
300
- JOIN queen.partitions p2 ON p2.id = m2.partition_id
301
- LEFT JOIN queen.partition_consumers pc2 ON pc2.partition_id = p2.id
302
- AND pc2.consumer_group = '__QUEUE_MODE__'
303
- WHERE p2.queue_id = q.id
304
- AND (m2.created_at, m2.id) > (COALESCE(pc2.last_consumed_created_at, '1970-01-01'::timestamptz),
305
- COALESCE(pc2.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
306
- ) as lag_seconds,
307
- 0 as avg_processing_time_seconds
308
- FROM queen.queues q
309
- LEFT JOIN queen.partitions p ON p.queue_id = q.id
310
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
311
- AND pc.consumer_group = '__QUEUE_MODE__'
312
- WHERE 1=1 ${whereClause}
313
- GROUP BY q.id, q.name, q.namespace, q.task, q.priority, q.created_at
314
- ORDER BY q.priority DESC, q.name
315
- LIMIT $${paramCount} OFFSET $${paramCount + 1}
316
- `;
317
-
318
- const countQuery = `
319
- SELECT COUNT(DISTINCT q.id) as total
320
- FROM queen.queues q
321
- WHERE 1=1 ${whereClause}
322
- `;
323
-
324
- const [result, countResult] = await Promise.all([
325
- pool.query(query, params),
326
- pool.query(countQuery, params.slice(0, -2))
327
- ]);
328
-
329
- const queues = result.rows.map(row => {
330
- const total = parseInt(row.total_messages || 0);
331
- const completed = parseInt(row.completed || 0);
332
- const processing = parseInt(row.processing || 0);
333
- const failed = parseInt(row.failed || 0);
334
- // Calculate pending as: total - completed - processing - failed
335
- const pending = Math.max(0, total - completed - processing - failed);
336
-
337
- return {
338
- id: row.id,
339
- name: row.name,
340
- namespace: row.namespace,
341
- task: row.task,
342
- priority: row.priority,
343
- createdAt: row.created_at,
344
- partitions: parseInt(row.partition_count),
345
- messages: {
346
- total,
347
- pending,
348
- processing,
349
- completed,
350
- failed,
351
- deadLetter: parseInt(row.dead_letter || 0)
352
- },
353
- lag: row.lag_seconds ? {
354
- seconds: parseFloat(row.lag_seconds),
355
- formatted: formatDuration(parseFloat(row.lag_seconds))
356
- } : null,
357
- performance: row.avg_processing_time_seconds ? {
358
- avgProcessingTimeSeconds: parseFloat(row.avg_processing_time_seconds),
359
- avgProcessingTimeFormatted: formatDuration(parseFloat(row.avg_processing_time_seconds))
360
- } : null
361
- };
362
- });
363
-
364
- return {
365
- queues,
366
- pagination: {
367
- limit: parseInt(limit),
368
- offset: parseInt(offset),
369
- total: parseInt(countResult.rows[0]?.total || 0)
370
- },
371
- timeRange: {
372
- from: timeRange.from.toISOString(),
373
- to: timeRange.to.toISOString()
374
- }
375
- };
376
- };
377
-
378
- // 3. Queue Detail Endpoint
379
- const getQueueDetail = async (queueName, filters = {}) => {
380
- const { from, to } = filters;
381
- const timeRange = getTimeRange(from, to);
382
-
383
- const query = `
384
- SELECT
385
- q.id, q.name, q.namespace, q.task, q.priority,
386
- q.lease_time, q.retry_limit, q.ttl, q.max_queue_size,
387
- q.created_at,
388
- p.id as partition_id,
389
- p.name as partition_name,
390
- p.created_at as partition_created_at,
391
- p.last_activity as partition_last_activity,
392
- COUNT(DISTINCT m.id) as partition_total_messages,
393
- -- Calculate pending dynamically: count messages after last consumed
394
- (SELECT COUNT(*) FROM queen.messages m2
395
- WHERE m2.partition_id = p.id
396
- AND (m2.created_at, m2.id) > (COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz),
397
- COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
398
- ) as partition_pending,
399
- CASE WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 1 ELSE 0 END as partition_processing,
400
- COALESCE(pc.total_messages_consumed, 0) as partition_completed,
401
- 0 as partition_failed,
402
- pc.total_messages_consumed,
403
- pc.total_batches_consumed,
404
- pc.last_consumed_at,
405
- pc.lease_expires_at,
406
- pc.batch_size as lease_batch_size,
407
- pc.acked_count as lease_acked_count,
408
- (SELECT MIN(created_at) FROM queen.messages m2
409
- WHERE m2.partition_id = p.id
410
- AND (m2.created_at, m2.id) > (COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz),
411
- COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
412
- ) as oldest_pending,
413
- MAX(m.created_at) as newest_message
414
- FROM queen.queues q
415
- LEFT JOIN queen.partitions p ON p.queue_id = q.id
416
- LEFT JOIN queen.messages m ON m.partition_id = p.id
417
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
418
- AND pc.consumer_group = '__QUEUE_MODE__'
419
- WHERE q.name = $1
420
- GROUP BY q.id, q.name, q.namespace, q.task, q.priority, q.lease_time,
421
- q.retry_limit, q.ttl, q.max_queue_size, q.created_at,
422
- p.id, p.name, p.created_at, p.last_activity,
423
- pc.total_messages_consumed, pc.total_batches_consumed, pc.last_consumed_at,
424
- pc.lease_expires_at, pc.batch_size, pc.acked_count, pc.last_consumed_created_at, pc.last_consumed_id
425
- ORDER BY p.name
426
- `;
427
-
428
- const result = await pool.query(query, [queueName]);
429
-
430
- if (result.rows.length === 0) {
431
- throw new Error('Queue not found');
432
- }
433
-
434
- const firstRow = result.rows[0];
435
- const queue = {
436
- id: firstRow.id,
437
- name: firstRow.name,
438
- namespace: firstRow.namespace,
439
- task: firstRow.task,
440
- priority: firstRow.priority,
441
- config: {
442
- leaseTime: firstRow.lease_time,
443
- retryLimit: firstRow.retry_limit,
444
- ttl: firstRow.ttl,
445
- maxQueueSize: firstRow.max_queue_size
446
- },
447
- createdAt: firstRow.created_at
448
- };
449
-
450
- // Aggregate totals
451
- const totals = {
452
- messages: {
453
- total: 0,
454
- pending: 0,
455
- processing: 0,
456
- completed: 0,
457
- failed: 0
458
- },
459
- partitions: result.rows.filter(r => r.partition_id).length,
460
- consumed: 0,
461
- batches: 0
462
- };
463
-
464
- const partitions = result.rows
465
- .filter(row => row.partition_id)
466
- .map(row => {
467
- const total = parseInt(row.partition_total_messages || 0);
468
- const completed = parseInt(row.partition_completed || 0);
469
- const processing = parseInt(row.partition_processing || 0);
470
- const failed = parseInt(row.partition_failed || 0);
471
- // Calculate pending correctly: total - completed - processing - failed
472
- const pending = Math.max(0, total - completed - processing - failed);
473
-
474
- totals.messages.total += total;
475
- totals.messages.pending += pending;
476
- totals.messages.processing += processing;
477
- totals.messages.completed += completed;
478
- totals.messages.failed += failed;
479
- totals.consumed += parseInt(row.total_messages_consumed || 0);
480
- totals.batches += parseInt(row.total_batches_consumed || 0);
481
-
482
- const partition = {
483
- id: row.partition_id,
484
- name: row.partition_name,
485
- createdAt: row.partition_created_at,
486
- lastActivity: row.partition_last_activity,
487
- messages: {
488
- total,
489
- pending,
490
- processing,
491
- completed,
492
- failed
493
- },
494
- cursor: {
495
- totalConsumed: parseInt(row.total_messages_consumed || 0),
496
- batchesConsumed: parseInt(row.total_batches_consumed || 0),
497
- lastConsumedAt: row.last_consumed_at
498
- }
499
- };
500
-
501
- if (row.lease_expires_at) {
502
- partition.lease = {
503
- active: new Date(row.lease_expires_at) > new Date(),
504
- expiresAt: row.lease_expires_at,
505
- batchSize: parseInt(row.lease_batch_size || 0),
506
- ackedCount: parseInt(row.lease_acked_count || 0)
507
- };
508
- }
509
-
510
- if (row.oldest_pending) {
511
- const lagSeconds = (new Date() - new Date(row.oldest_pending)) / 1000;
512
- partition.lag = {
513
- oldestPending: row.oldest_pending,
514
- seconds: parseFloat(lagSeconds.toFixed(1)),
515
- formatted: formatDuration(parseFloat(lagSeconds.toFixed(1)))
516
- };
517
- }
518
-
519
- return partition;
520
- });
521
-
522
- return {
523
- queue,
524
- totals,
525
- partitions,
526
- timeRange: {
527
- from: timeRange.from.toISOString(),
528
- to: timeRange.to.toISOString()
529
- }
530
- };
531
- };
532
-
533
- // 4. Queue Messages Endpoint
534
- const getQueueMessages = async (queueName, filters = {}) => {
535
- const { status, partition, from, to, limit = 50, offset = 0 } = filters;
536
- const timeRange = getTimeRange(from, to);
537
-
538
- const conditions = ['q.name = $1', 'm.created_at >= $2', 'm.created_at <= $3'];
539
- const params = [queueName, timeRange.from, timeRange.to];
540
- let paramCount = 4;
541
-
542
- // Note: V2 schema doesn't have a messages_status table
543
- // Status must be inferred from partition_consumers and dead_letter_queue
544
- if (status) {
545
- if (status === 'failed') {
546
- conditions.push(`EXISTS (SELECT 1 FROM queen.dead_letter_queue dlq WHERE dlq.message_id = m.id)`);
547
- } else if (status === 'completed') {
548
- conditions.push(`(m.created_at < pc.last_consumed_created_at OR (DATE_TRUNC('milliseconds', m.created_at) = DATE_TRUNC('milliseconds', pc.last_consumed_created_at) AND m.id <= pc.last_consumed_id))`);
549
- } else if (status === 'pending') {
550
- conditions.push(`(m.created_at > COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz) OR (DATE_TRUNC('milliseconds', m.created_at) = DATE_TRUNC('milliseconds', COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz)) AND m.id > COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid)))`);
551
- }
552
- }
553
-
554
- if (partition) {
555
- conditions.push(`p.name = $${paramCount}`);
556
- params.push(partition);
557
- paramCount++;
558
- }
559
-
560
- params.push(limit, offset);
561
-
562
- const query = `
563
- SELECT
564
- m.id,
565
- m.transaction_id,
566
- m.trace_id,
567
- m.created_at,
568
- m.is_encrypted,
569
- m.payload,
570
- p.name as partition,
571
- CASE
572
- WHEN EXISTS (SELECT 1 FROM queen.dead_letter_queue dlq WHERE dlq.message_id = m.id) THEN 'failed'
573
- WHEN (m.created_at < COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz) OR (DATE_TRUNC('milliseconds', m.created_at) = DATE_TRUNC('milliseconds', COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz)) AND m.id <= COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))) THEN 'completed'
574
- WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
575
- ELSE 'pending'
576
- END as status,
577
- pc.worker_id,
578
- pc.lease_acquired_at as locked_at,
579
- pc.last_consumed_at as completed_at,
580
- dlq.failed_at,
581
- dlq.error_message,
582
- dlq.retry_count,
583
- pc.lease_acquired_at as processing_at,
584
- NULL::numeric as processing_time_seconds,
585
- EXTRACT(EPOCH FROM (NOW() - m.created_at)) as age_seconds
586
- FROM queen.messages m
587
- JOIN queen.partitions p ON p.id = m.partition_id
588
- JOIN queen.queues q ON q.id = p.queue_id
589
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
590
- AND pc.consumer_group = '__QUEUE_MODE__'
591
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
592
- AND dlq.consumer_group = '__QUEUE_MODE__'
593
- WHERE ${conditions.join(' AND ')}
594
- ORDER BY m.created_at DESC
595
- LIMIT $${paramCount} OFFSET $${paramCount + 1}
596
- `;
597
-
598
- const countQuery = `
599
- SELECT COUNT(*) as total
600
- FROM queen.messages m
601
- JOIN queen.partitions p ON p.id = m.partition_id
602
- JOIN queen.queues q ON q.id = p.queue_id
603
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
604
- AND pc.consumer_group = '__QUEUE_MODE__'
605
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
606
- AND dlq.consumer_group = '__QUEUE_MODE__'
607
- WHERE ${conditions.join(' AND ')}
608
- `;
609
-
610
- const [result, countResult] = await Promise.all([
611
- pool.query(query, params),
612
- pool.query(countQuery, params.slice(0, -2))
613
- ]);
614
-
615
- const messages = result.rows.map(row => {
616
- const message = {
617
- id: row.id,
618
- transactionId: row.transaction_id,
619
- traceId: row.trace_id,
620
- partition: row.partition,
621
- createdAt: row.created_at,
622
- status: row.status,
623
- retryCount: row.retry_count || 0,
624
- isEncrypted: row.is_encrypted
625
- };
626
-
627
- if (!row.is_encrypted) {
628
- message.data = row.payload;
629
- }
630
-
631
- if (row.status === 'pending' || row.status === 'processing') {
632
- message.age = {
633
- seconds: parseFloat(row.age_seconds),
634
- formatted: formatDuration(parseFloat(row.age_seconds))
635
- };
636
- }
637
-
638
- if (row.status === 'completed' || row.status === 'failed') {
639
- message.completedAt = row.completed_at || row.failed_at;
640
- if (row.processing_time_seconds) {
641
- message.processingTime = {
642
- seconds: parseFloat(row.processing_time_seconds),
643
- formatted: formatDuration(parseFloat(row.processing_time_seconds))
644
- };
645
- }
646
- }
647
-
648
- if (row.worker_id) {
649
- message.workerId = row.worker_id;
650
- }
651
-
652
- if (row.error_message) {
653
- message.errorMessage = row.error_message;
654
- }
655
-
656
- return message;
657
- });
658
-
659
- return {
660
- messages,
661
- pagination: {
662
- limit: parseInt(limit),
663
- offset: parseInt(offset),
664
- total: parseInt(countResult.rows[0]?.total || 0)
665
- },
666
- queue: queueName,
667
- filters: {
668
- status: status || null,
669
- partition: partition || null
670
- },
671
- timeRange: {
672
- from: timeRange.from.toISOString(),
673
- to: timeRange.to.toISOString()
674
- }
675
- };
676
- };
677
-
678
- // 5. Analytics Endpoint
679
- const getAnalytics = async (filters = {}) => {
680
- const { from, to, queue, namespace, task, interval = 'hour' } = filters;
681
-
682
- // Default to last 24 hours for analytics
683
- const toDate = to ? new Date(to) : new Date();
684
- const fromDate = from ? new Date(from) : new Date(toDate.getTime() - 24 * 60 * 60 * 1000);
685
-
686
- // Determine interval
687
- const intervalMap = {
688
- 'minute': '1 minute',
689
- 'hour': '1 hour',
690
- 'day': '1 day'
691
- };
692
- const sqlInterval = intervalMap[interval] || '1 hour';
693
-
694
- const buildFilters = (startParam = 3) => {
695
- const conditions = [];
696
- const params = [fromDate, toDate];
697
- let paramCount = startParam;
698
-
699
- if (queue) {
700
- conditions.push(`q.name = $${paramCount}`);
701
- params.push(queue);
702
- paramCount++;
703
- }
704
- if (namespace) {
705
- conditions.push(`q.namespace = $${paramCount}`);
706
- params.push(namespace);
707
- paramCount++;
708
- }
709
- if (task) {
710
- conditions.push(`q.task = $${paramCount}`);
711
- params.push(task);
712
- paramCount++;
713
- }
714
-
715
- return { conditions, params, whereClause: conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : '' };
716
- };
717
-
718
- const filterInfo = buildFilters(3);
719
-
720
- try {
721
- // Query 1: Throughput time series
722
- const throughputQuery = `
723
- WITH time_series AS (
724
- SELECT generate_series(
725
- DATE_TRUNC('${interval}', $1::timestamptz),
726
- DATE_TRUNC('${interval}', $2::timestamptz),
727
- '${sqlInterval}'::interval
728
- ) AS bucket
729
- ),
730
- ingested_counts AS (
731
- SELECT
732
- DATE_TRUNC('${interval}', m.created_at) as bucket,
733
- COUNT(DISTINCT m.id) as ingested
734
- FROM queen.messages m
735
- JOIN queen.partitions p ON p.id = m.partition_id
736
- JOIN queen.queues q ON q.id = p.queue_id
737
- WHERE m.created_at >= $1
738
- AND m.created_at <= $2
739
- ${filterInfo.whereClause}
740
- GROUP BY DATE_TRUNC('${interval}', m.created_at)
741
- ),
742
- processed_counts AS (
743
- SELECT
744
- DATE_TRUNC('${interval}', mc.acked_at) as bucket,
745
- SUM(mc.messages_completed) as processed
746
- FROM queen.messages_consumed mc
747
- JOIN queen.partitions p ON p.id = mc.partition_id
748
- JOIN queen.queues q ON q.id = p.queue_id
749
- WHERE mc.acked_at >= $1::timestamptz
750
- AND mc.acked_at <= $2::timestamptz
751
- ${filterInfo.whereClause}
752
- GROUP BY DATE_TRUNC('${interval}', mc.acked_at)
753
- ),
754
- failed_counts AS (
755
- SELECT
756
- DATE_TRUNC('${interval}', dlq.failed_at) as bucket,
757
- COUNT(*) as failed
758
- FROM queen.dead_letter_queue dlq
759
- JOIN queen.partitions p ON p.id = dlq.partition_id
760
- JOIN queen.queues q ON q.id = p.queue_id
761
- WHERE dlq.failed_at >= $1::timestamptz
762
- AND dlq.failed_at <= $2::timestamptz
763
- AND (dlq.consumer_group IS NULL OR dlq.consumer_group = '__QUEUE_MODE__')
764
- ${filterInfo.whereClause}
765
- GROUP BY DATE_TRUNC('${interval}', dlq.failed_at)
766
- )
767
- SELECT
768
- ts.bucket as timestamp,
769
- COALESCE(ic.ingested, 0) as ingested,
770
- COALESCE(pc.processed, 0) as processed,
771
- COALESCE(fc.failed, 0) as failed
772
- FROM time_series ts
773
- LEFT JOIN ingested_counts ic ON ic.bucket = ts.bucket
774
- LEFT JOIN processed_counts pc ON pc.bucket = ts.bucket
775
- LEFT JOIN failed_counts fc ON fc.bucket = ts.bucket
776
- ORDER BY ts.bucket DESC
777
- `;
778
-
779
- // Query 2: Latency percentiles time series
780
- // Note: In V2 schema, we don't track individual message completion times
781
- // This would require additional instrumentation. For now, return empty latency data.
782
- const latencyQuery = `
783
- WITH time_series AS (
784
- SELECT generate_series(
785
- DATE_TRUNC('${interval}', $1::timestamptz),
786
- DATE_TRUNC('${interval}', $2::timestamptz),
787
- '${sqlInterval}'::interval
788
- ) AS bucket
789
- )
790
- SELECT
791
- ts.bucket as timestamp,
792
- NULL::numeric as p50,
793
- NULL::numeric as p95,
794
- NULL::numeric as p99,
795
- NULL::numeric as avg,
796
- NULL::numeric as min,
797
- NULL::numeric as max,
798
- 0 as sample_count
799
- FROM time_series ts
800
- ORDER BY ts.bucket DESC
801
- `;
802
-
803
- // Query 3: Top queues by volume (based on ingested messages in time range)
804
- const topQueuesQuery = `
805
- SELECT
806
- q.name,
807
- q.namespace,
808
- COUNT(DISTINCT m.id) as messages_ingested,
809
- COALESCE(SUM(DISTINCT pc.total_messages_consumed), 0) as total_consumed_lifetime,
810
- COUNT(DISTINCT dlq.id) as failed_in_range,
811
- COUNT(DISTINCT dlq.id)::float / NULLIF(COUNT(DISTINCT m.id), 0) as error_rate
812
- FROM queen.queues q
813
- JOIN queen.partitions p ON p.queue_id = q.id
814
- LEFT JOIN queen.messages m ON m.partition_id = p.id
815
- AND m.created_at >= $1 AND m.created_at <= $2
816
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
817
- AND pc.consumer_group = '__QUEUE_MODE__'
818
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.partition_id = p.id
819
- AND dlq.consumer_group = '__QUEUE_MODE__'
820
- AND dlq.failed_at >= $1 AND dlq.failed_at <= $2
821
- WHERE 1=1 ${filterInfo.whereClause}
822
- GROUP BY q.id, q.name, q.namespace
823
- HAVING COUNT(DISTINCT m.id) > 0
824
- ORDER BY messages_ingested DESC
825
- LIMIT 10
826
- `;
827
-
828
- // Query 4: DLQ time series
829
- const dlqQuery = `
830
- WITH time_series AS (
831
- SELECT generate_series(
832
- DATE_TRUNC('${interval}', $1::timestamptz),
833
- DATE_TRUNC('${interval}', $2::timestamptz),
834
- '${sqlInterval}'::interval
835
- ) AS bucket
836
- ),
837
- dlq_counts AS (
838
- SELECT
839
- DATE_TRUNC('${interval}', dlq.failed_at) as bucket,
840
- COUNT(*) as messages
841
- FROM queen.dead_letter_queue dlq
842
- JOIN queen.partitions p ON p.id = dlq.partition_id
843
- JOIN queen.queues q ON q.id = p.queue_id
844
- WHERE dlq.failed_at >= $1::timestamptz
845
- AND dlq.failed_at <= $2::timestamptz
846
- AND (dlq.consumer_group IS NULL OR dlq.consumer_group = '__QUEUE_MODE__')
847
- ${filterInfo.whereClause}
848
- GROUP BY DATE_TRUNC('${interval}', dlq.failed_at)
849
- )
850
- SELECT
851
- ts.bucket as timestamp,
852
- COALESCE(dc.messages, 0) as messages
853
- FROM time_series ts
854
- LEFT JOIN dlq_counts dc ON dc.bucket = ts.bucket
855
- ORDER BY ts.bucket DESC
856
- `;
857
-
858
- // Query 5: DLQ top errors
859
- const dlqErrorsQuery = `
860
- SELECT
861
- error_message,
862
- COUNT(*) as count
863
- FROM queen.dead_letter_queue dlq
864
- LEFT JOIN queen.partitions p ON p.id = dlq.partition_id
865
- LEFT JOIN queen.queues q ON q.id = p.queue_id
866
- WHERE dlq.failed_at >= $1 AND dlq.failed_at <= $2
867
- AND (dlq.consumer_group IS NULL OR dlq.consumer_group = '__QUEUE_MODE__')
868
- ${filterInfo.whereClause}
869
- GROUP BY error_message
870
- ORDER BY count DESC
871
- LIMIT 10
872
- `;
873
-
874
- // Execute all queries in parallel
875
- const [throughputResult, latencyResult, topQueuesResult, dlqResult, dlqErrorsResult] = await Promise.all([
876
- pool.query(throughputQuery, filterInfo.params),
877
- pool.query(latencyQuery, filterInfo.params.slice(0, 2)),
878
- pool.query(topQueuesQuery, filterInfo.params),
879
- pool.query(dlqQuery, filterInfo.params),
880
- pool.query(dlqErrorsQuery, filterInfo.params)
881
- ]);
882
-
883
- // Calculate seconds per bucket for rate calculations
884
- const secondsPerBucket = interval === 'minute' ? 60 : interval === 'hour' ? 3600 : 86400;
885
-
886
- // Format throughput data
887
- const throughputTimeSeries = throughputResult.rows.map(row => ({
888
- timestamp: row.timestamp,
889
- ingested: parseInt(row.ingested || 0),
890
- processed: parseInt(row.processed || 0),
891
- failed: parseInt(row.failed || 0),
892
- ingestedPerSecond: parseFloat((parseInt(row.ingested || 0) / secondsPerBucket).toFixed(2)),
893
- processedPerSecond: parseFloat((parseInt(row.processed || 0) / secondsPerBucket).toFixed(2))
894
- }));
895
-
896
- const throughputTotals = throughputTimeSeries.reduce((acc, row) => ({
897
- ingested: acc.ingested + row.ingested,
898
- processed: acc.processed + row.processed,
899
- failed: acc.failed + row.failed
900
- }), { ingested: 0, processed: 0, failed: 0 });
901
-
902
- const totalSeconds = ((toDate - fromDate) / 1000);
903
- throughputTotals.avgIngestedPerSecond = parseFloat((throughputTotals.ingested / totalSeconds).toFixed(2));
904
- throughputTotals.avgProcessedPerSecond = parseFloat((throughputTotals.processed / totalSeconds).toFixed(2));
905
-
906
- // Format latency data
907
- const latencyTimeSeries = latencyResult.rows
908
- .filter(row => row.sample_count > 0)
909
- .map(row => ({
910
- timestamp: row.timestamp,
911
- p50: parseFloat(row.p50 || 0),
912
- p95: parseFloat(row.p95 || 0),
913
- p99: parseFloat(row.p99 || 0),
914
- avg: parseFloat(row.avg || 0),
915
- min: parseFloat(row.min || 0),
916
- max: parseFloat(row.max || 0)
917
- }));
918
-
919
- // Calculate overall latency
920
- const allSamples = latencyResult.rows.filter(row => row.sample_count > 0);
921
- const latencyOverall = allSamples.length > 0 ? {
922
- p50: parseFloat((allSamples.reduce((sum, row) => sum + parseFloat(row.p50 || 0), 0) / allSamples.length).toFixed(2)),
923
- p95: parseFloat((allSamples.reduce((sum, row) => sum + parseFloat(row.p95 || 0), 0) / allSamples.length).toFixed(2)),
924
- p99: parseFloat((allSamples.reduce((sum, row) => sum + parseFloat(row.p99 || 0), 0) / allSamples.length).toFixed(2)),
925
- avg: parseFloat((allSamples.reduce((sum, row) => sum + parseFloat(row.avg || 0), 0) / allSamples.length).toFixed(2))
926
- } : null;
927
-
928
- // Format error rates
929
- const errorRatesTimeSeries = throughputTimeSeries.map(row => {
930
- const total = row.processed + row.failed;
931
- const rate = total > 0 ? row.failed / total : 0;
932
- return {
933
- timestamp: row.timestamp,
934
- failed: row.failed,
935
- processed: row.processed,
936
- rate: parseFloat(rate.toFixed(4)),
937
- ratePercent: `${(rate * 100).toFixed(2)}%`
938
- };
939
- });
940
-
941
- const errorRatesOverall = {
942
- failed: throughputTotals.failed,
943
- processed: throughputTotals.processed,
944
- rate: throughputTotals.processed > 0 ?
945
- parseFloat((throughputTotals.failed / throughputTotals.processed).toFixed(4)) : 0
946
- };
947
- errorRatesOverall.ratePercent = `${(errorRatesOverall.rate * 100).toFixed(2)}%`;
948
-
949
- // Format top queues
950
- const topQueues = topQueuesResult.rows.map(row => ({
951
- name: row.name,
952
- namespace: row.namespace,
953
- messagesIngested: parseInt(row.messages_ingested || 0),
954
- totalConsumedLifetime: parseInt(row.total_consumed_lifetime || 0),
955
- failedInRange: parseInt(row.failed_in_range || 0),
956
- errorRate: parseFloat(row.error_rate || 0)
957
- }));
958
-
959
- // Format DLQ data
960
- const dlqTimeSeries = dlqResult.rows.map(row => ({
961
- timestamp: row.timestamp,
962
- messages: parseInt(row.messages || 0)
963
- }));
964
-
965
- const dlqTotal = dlqTimeSeries.reduce((sum, row) => sum + row.messages, 0);
966
- const totalErrors = dlqErrorsResult.rows.reduce((sum, row) => sum + parseInt(row.count || 0), 0);
967
-
968
- const dlqTopErrors = dlqErrorsResult.rows.map(row => ({
969
- error: row.error_message,
970
- count: parseInt(row.count || 0),
971
- percentage: totalErrors > 0 ? parseFloat(((parseInt(row.count || 0) / totalErrors) * 100).toFixed(1)) : 0
972
- }));
973
-
974
- // Calculate queue depths (current state)
975
- const depthFilters = buildFilters(1);
976
- const depthQuery = `
977
- SELECT
978
- COALESCE(SUM(pc.pending_estimate), 0) as pending,
979
- COALESCE(SUM(CASE WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN pc.batch_size ELSE 0 END), 0) as processing
980
- FROM queen.queues q
981
- LEFT JOIN queen.partitions p ON p.queue_id = q.id
982
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
983
- AND pc.consumer_group = '__QUEUE_MODE__'
984
- WHERE 1=1 ${depthFilters.whereClause}
985
- `;
986
-
987
- const depthResult = await pool.query(depthQuery, depthFilters.params.slice(2));
988
- const currentDepth = {
989
- pending: parseInt(depthResult.rows[0]?.pending || 0),
990
- processing: parseInt(depthResult.rows[0]?.processing || 0)
991
- };
992
-
993
- return {
994
- timeRange: {
995
- from: fromDate.toISOString(),
996
- to: toDate.toISOString(),
997
- interval
998
- },
999
- throughput: {
1000
- timeSeries: throughputTimeSeries,
1001
- totals: throughputTotals
1002
- },
1003
- latency: {
1004
- timeSeries: latencyTimeSeries,
1005
- overall: latencyOverall
1006
- },
1007
- errorRates: {
1008
- timeSeries: errorRatesTimeSeries,
1009
- overall: errorRatesOverall
1010
- },
1011
- queueDepths: {
1012
- timeSeries: [], // Could add historical depth tracking if needed
1013
- current: currentDepth
1014
- },
1015
- topQueues,
1016
- deadLetterQueue: {
1017
- timeSeries: dlqTimeSeries,
1018
- total: dlqTotal,
1019
- topErrors: dlqTopErrors
1020
- }
1021
- };
1022
-
1023
- } catch (error) {
1024
- console.error('Error in getAnalytics:', error);
1025
- throw error;
1026
- }
1027
- };
1028
-
1029
- return {
1030
- getStatus,
1031
- getQueues,
1032
- getQueueDetail,
1033
- getQueueMessages,
1034
- getAnalytics
1035
- };
1036
- };
1037
-