import { Queue, QueueEvents, Worker } from 'bullmq'; import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { QueueMetricsCollector } from '../src/queue-metrics'; import { getRedisConnection } from '../src/utils'; // Suppress Redis connection errors in tests process.on('unhandledRejection', (reason, promise) => { if (reason && typeof reason === 'object' && 'message' in reason) { const message = (reason as Error).message; if ( message.includes('Connection is closed') || message.includes('Connection is in monitoring mode') ) { return; } } console.error('Unhandled Rejection at:', promise, 'reason:', reason); }); describe('QueueMetricsCollector', () => { let queue: Queue; let queueEvents: QueueEvents; let metricsCollector: QueueMetricsCollector; let worker: Worker; let connection: any; const redisConfig = { host: 'localhost', port: 6379, password: '', db: 0, }; beforeEach(async () => { connection = getRedisConnection(redisConfig); // Create queue and events queue = new Queue('metrics-test-queue', { connection }); queueEvents = new QueueEvents('metrics-test-queue', { connection }); // Create metrics collector metricsCollector = new QueueMetricsCollector(queue, queueEvents); // Wait for connections await queue.waitUntilReady(); await queueEvents.waitUntilReady(); }); afterEach(async () => { try { if (worker) { await worker.close(); } await queueEvents.close(); await queue.close(); } catch { // Ignore cleanup errors } await new Promise(resolve => setTimeout(resolve, 50)); }); describe('Job Count Metrics', () => { test('should collect basic job counts', async () => { // Add jobs in different states await queue.add('waiting-job', { test: true }); await queue.add('delayed-job', { test: true }, { delay: 60000 }); const metrics = await metricsCollector.collect(); expect(metrics.waiting).toBe(1); expect(metrics.delayed).toBe(1); expect(metrics.active).toBe(0); expect(metrics.completed).toBe(0); expect(metrics.failed).toBe(0); }); test('should track completed and failed jobs', async () => { let jobCount = 0; // Create worker that alternates between success and failure worker = new Worker( 'metrics-test-queue', async () => { jobCount++; if (jobCount % 2 === 0) { throw new Error('Test failure'); } return { success: true }; }, { connection } ); // Add jobs await queue.add('job1', { test: 1 }); await queue.add('job2', { test: 2 }); await queue.add('job3', { test: 3 }); await queue.add('job4', { test: 4 }); // Wait for processing await new Promise(resolve => setTimeout(resolve, 200)); const metrics = await metricsCollector.collect(); expect(metrics.completed).toBe(2); expect(metrics.failed).toBe(2); }); }); describe('Processing Time Metrics', () => { test('should track processing times', async () => { const processingTimes = [50, 100, 150, 200, 250]; let jobIndex = 0; // Create worker with variable processing times worker = new Worker( 'metrics-test-queue', async () => { const delay = processingTimes[jobIndex++] || 100; await new Promise(resolve => setTimeout(resolve, delay)); return { processed: true }; }, { connection } ); // Add jobs for (let i = 0; i < processingTimes.length; i++) { await queue.add(`job${i}`, { index: i }); } // Wait for processing await new Promise(resolve => setTimeout(resolve, 1500)); const metrics = await metricsCollector.collect(); expect(metrics.processingTime.avg).toBeGreaterThan(0); expect(metrics.processingTime.min).toBeGreaterThanOrEqual(50); expect(metrics.processingTime.max).toBeLessThanOrEqual(300); expect(metrics.processingTime.p95).toBeGreaterThan(metrics.processingTime.avg); }); test('should handle empty processing times', async () => { const metrics = await metricsCollector.collect(); expect(metrics.processingTime).toEqual({ avg: 0, min: 0, max: 0, p95: 0, p99: 0, }); }); }); describe('Throughput Metrics', () => { test('should calculate throughput correctly', async () => { // Create fast worker worker = new Worker( 'metrics-test-queue', async () => { return { success: true }; }, { connection, concurrency: 5 } ); // Add multiple jobs const jobPromises = []; for (let i = 0; i < 10; i++) { jobPromises.push(queue.add(`job${i}`, { index: i })); } await Promise.all(jobPromises); // Wait for processing await new Promise(resolve => setTimeout(resolve, 500)); const metrics = await metricsCollector.collect(); expect(metrics.throughput.completedPerMinute).toBeGreaterThan(0); expect(metrics.throughput.totalPerMinute).toBe( metrics.throughput.completedPerMinute + metrics.throughput.failedPerMinute ); }); }); describe('Queue Health', () => { test('should report healthy queue', async () => { const metrics = await metricsCollector.collect(); expect(metrics.isHealthy).toBe(true); expect(metrics.healthIssues).toEqual([]); }); test('should detect high failure rate', async () => { // Create worker that always fails worker = new Worker( 'metrics-test-queue', async () => { throw new Error('Always fails'); }, { connection } ); // Add jobs for (let i = 0; i < 10; i++) { await queue.add(`job${i}`, { index: i }); } // Wait for failures await new Promise(resolve => setTimeout(resolve, 500)); const metrics = await metricsCollector.collect(); expect(metrics.isHealthy).toBe(false); expect(metrics.healthIssues).toContain(expect.stringMatching(/High failure rate/)); }); test('should detect large queue backlog', async () => { // Add many jobs without workers for (let i = 0; i < 1001; i++) { await queue.add(`job${i}`, { index: i }); } const metrics = await metricsCollector.collect(); expect(metrics.isHealthy).toBe(false); expect(metrics.healthIssues).toContain(expect.stringMatching(/Large queue backlog/)); }); }); describe('Oldest Waiting Job', () => { test('should track oldest waiting job', async () => { const beforeAdd = Date.now(); // Add jobs with delays await queue.add('old-job', { test: true }); await new Promise(resolve => setTimeout(resolve, 100)); await queue.add('new-job', { test: true }); const metrics = await metricsCollector.collect(); expect(metrics.oldestWaitingJob).toBeDefined(); expect(metrics.oldestWaitingJob!.getTime()).toBeGreaterThanOrEqual(beforeAdd); }); test('should return null when no waiting jobs', async () => { // Create worker that processes immediately worker = new Worker( 'metrics-test-queue', async () => { return { success: true }; }, { connection } ); const metrics = await metricsCollector.collect(); expect(metrics.oldestWaitingJob).toBe(null); }); }); describe('Metrics Report', () => { test('should generate formatted report', async () => { // Add some jobs await queue.add('job1', { test: true }); await queue.add('job2', { test: true }, { delay: 5000 }); const report = await metricsCollector.getReport(); expect(report).toContain('Queue Metrics Report'); expect(report).toContain('Status:'); expect(report).toContain('Job Counts:'); expect(report).toContain('Performance:'); expect(report).toContain('Throughput:'); expect(report).toContain('Waiting: 1'); expect(report).toContain('Delayed: 1'); }); test('should include health issues in report', async () => { // Add many jobs to trigger health issue for (let i = 0; i < 1001; i++) { await queue.add(`job${i}`, { index: i }); } const report = await metricsCollector.getReport(); expect(report).toContain('Issues Detected'); expect(report).toContain('Health Issues:'); expect(report).toContain('Large queue backlog'); }); }); describe('Prometheus Metrics', () => { test('should export metrics in Prometheus format', async () => { // Add some jobs and process them worker = new Worker( 'metrics-test-queue', async () => { await new Promise(resolve => setTimeout(resolve, 50)); return { success: true }; }, { connection } ); await queue.add('job1', { test: true }); await queue.add('job2', { test: true }); // Wait for processing await new Promise(resolve => setTimeout(resolve, 200)); const prometheusMetrics = await metricsCollector.getPrometheusMetrics(); // Check format expect(prometheusMetrics).toContain('# HELP queue_jobs_total'); expect(prometheusMetrics).toContain('# TYPE queue_jobs_total gauge'); expect(prometheusMetrics).toContain( 'queue_jobs_total{queue="metrics-test-queue",status="completed"}' ); expect(prometheusMetrics).toContain('# HELP queue_processing_time_seconds'); expect(prometheusMetrics).toContain('# TYPE queue_processing_time_seconds summary'); expect(prometheusMetrics).toContain('# HELP queue_throughput_per_minute'); expect(prometheusMetrics).toContain('# TYPE queue_throughput_per_minute gauge'); expect(prometheusMetrics).toContain('# HELP queue_health'); expect(prometheusMetrics).toContain('# TYPE queue_health gauge'); }); }); });