queen-mq 0.2.0 → 0.2.11
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/README.md +165 -769
- package/{src → client-js}/benchmark/consumer.js +5 -3
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/{src → client-js}/client/client.js +272 -6
- package/{src → client-js}/client/index.js +1 -4
- package/{src → client-js}/client/utils/http.js +3 -2
- package/{src → client-js}/client/utils/retry.js +7 -1
- package/{src → client-js}/test/core-tests.js +7 -2
- package/{src → client-js}/test/edge-case-tests.js +2 -1
- package/client-js/test/human.js +162 -0
- package/client-js/test/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +42 -0
- package/package.json +8 -17
- package/init-db.js +0 -20
- package/src/cluster-server.js +0 -242
- package/src/config.js +0 -229
- package/src/database/connection.js +0 -129
- package/src/database/poolManager.js +0 -199
- package/src/database/schema-v2.sql +0 -278
- package/src/managers/eventManager.js +0 -59
- package/src/managers/queueManagerOptimized.js +0 -1634
- package/src/managers/resourceCache.js +0 -96
- package/src/managers/systemEventManager.js +0 -132
- package/src/routes/ack.js +0 -26
- package/src/routes/configure.js +0 -46
- package/src/routes/messages.js +0 -368
- package/src/routes/pop.js +0 -69
- package/src/routes/push.js +0 -28
- package/src/routes/resources.js +0 -330
- package/src/routes/status.js +0 -1037
- package/src/server.js +0 -1633
- package/src/test/MIGRATION_ISSUES.md +0 -174
- package/src/test/test.js +0 -4524
- package/src/utils/streaming.js +0 -231
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
- package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
- package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
- package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
- package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
- package/src/webapp-dist/assets/queen-logo.svg +0 -263
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
- package/src/webapp-dist/index.html +0 -15
- package/src/websocket/wsServer.js +0 -228
- /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
- /package/{src → client-js}/benchmark/producer_multi.js +0 -0
- /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
- /package/{src → client-js}/services/encryptionService.js +0 -0
- /package/{src → client-js}/services/evictionService.js +0 -0
- /package/{src → client-js}/services/retentionService.js +0 -0
- /package/{src → client-js}/services/startupSync.js +0 -0
- /package/{src → client-js}/test/README.md +0 -0
- /package/{src → client-js}/test/advanced-client-tests.js +0 -0
- /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
- /package/{src → client-js}/test/bus-mode-tests.js +0 -0
- /package/{src → client-js}/test/enterprise-tests.js +0 -0
- /package/{src → client-js}/test/partition-locking-tests.js +0 -0
- /package/{src → client-js}/test/utils.js +0 -0
- /package/{src → client-js}/test/window-buffer-test.js +0 -0
- /package/{src → client-js}/utils/logger.js +0 -0
- /package/{src → client-js}/utils/uuid.js +0 -0
package/src/routes/status.js
DELETED
|
@@ -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
|
-
|