185 lines
No EOL
6.9 KiB
TypeScript
185 lines
No EOL
6.9 KiB
TypeScript
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
import { QueueMetricsCollector } from '../src/queue-metrics';
|
|
import type { Queue, QueueEvents } from 'bullmq';
|
|
|
|
describe('QueueMetricsCollector', () => {
|
|
let metrics: QueueMetricsCollector;
|
|
|
|
const mockQueue = {
|
|
name: 'test-queue',
|
|
getWaitingCount: mock(() => Promise.resolve(0)),
|
|
getActiveCount: mock(() => Promise.resolve(0)),
|
|
getCompletedCount: mock(() => Promise.resolve(0)),
|
|
getFailedCount: mock(() => Promise.resolve(0)),
|
|
getDelayedCount: mock(() => Promise.resolve(0)),
|
|
isPaused: mock(() => Promise.resolve(false)),
|
|
getWaiting: mock(() => Promise.resolve([])),
|
|
} as unknown as Queue;
|
|
|
|
const mockQueueEvents = {
|
|
on: mock(() => {}),
|
|
} as unknown as QueueEvents;
|
|
|
|
beforeEach(() => {
|
|
metrics = new QueueMetricsCollector(mockQueue, mockQueueEvents);
|
|
});
|
|
|
|
describe('collect metrics', () => {
|
|
it('should collect current metrics', async () => {
|
|
(mockQueue.getWaitingCount as any) = mock(() => Promise.resolve(5));
|
|
(mockQueue.getActiveCount as any) = mock(() => Promise.resolve(2));
|
|
(mockQueue.getCompletedCount as any) = mock(() => Promise.resolve(100));
|
|
(mockQueue.getFailedCount as any) = mock(() => Promise.resolve(3));
|
|
(mockQueue.getDelayedCount as any) = mock(() => Promise.resolve(1));
|
|
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.waiting).toBe(5);
|
|
expect(result.active).toBe(2);
|
|
expect(result.completed).toBe(100);
|
|
expect(result.failed).toBe(3);
|
|
expect(result.delayed).toBe(1);
|
|
expect(result.isHealthy).toBe(true);
|
|
});
|
|
|
|
it('should detect health issues', async () => {
|
|
(mockQueue.getWaitingCount as any) = mock(() => Promise.resolve(2000)); // High backlog
|
|
(mockQueue.getActiveCount as any) = mock(() => Promise.resolve(150)); // High active
|
|
(mockQueue.getFailedCount as any) = mock(() => Promise.resolve(50));
|
|
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.isHealthy).toBe(false);
|
|
expect(result.healthIssues.length).toBeGreaterThan(0);
|
|
expect(result.healthIssues.some(issue => issue.includes('queue backlog'))).toBe(true);
|
|
expect(result.healthIssues.some(issue => issue.includes('active jobs'))).toBe(true);
|
|
});
|
|
|
|
it('should handle paused queue', async () => {
|
|
(mockQueue.getWaitingCount as any) = mock(() => Promise.resolve(10));
|
|
(mockQueue.isPaused as any) = mock(() => Promise.resolve(true));
|
|
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.paused).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('processing time metrics', () => {
|
|
it('should calculate processing time metrics', async () => {
|
|
// Simulate some processing times
|
|
(metrics as any).processingTimes = [1000, 2000, 3000, 4000, 5000];
|
|
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.processingTime.avg).toBe(3000);
|
|
expect(result.processingTime.min).toBe(1000);
|
|
expect(result.processingTime.max).toBe(5000);
|
|
expect(result.processingTime.p95).toBe(5000);
|
|
});
|
|
|
|
it('should handle no processing times', async () => {
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.processingTime.avg).toBe(0);
|
|
expect(result.processingTime.min).toBe(0);
|
|
expect(result.processingTime.max).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('throughput metrics', () => {
|
|
it('should calculate throughput', async () => {
|
|
// Simulate completed and failed timestamps
|
|
const now = Date.now();
|
|
(metrics as any).completedTimestamps = [
|
|
now - 30000, // 30 seconds ago
|
|
now - 20000,
|
|
now - 10000,
|
|
];
|
|
(metrics as any).failedTimestamps = [
|
|
now - 25000,
|
|
now - 5000,
|
|
];
|
|
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.throughput.completedPerMinute).toBe(3);
|
|
expect(result.throughput.failedPerMinute).toBe(2);
|
|
expect(result.throughput.totalPerMinute).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('getReport', () => {
|
|
it('should generate formatted report', async () => {
|
|
(mockQueue.getWaitingCount as any) = mock(() => Promise.resolve(5));
|
|
(mockQueue.getActiveCount as any) = mock(() => Promise.resolve(2));
|
|
(mockQueue.getCompletedCount as any) = mock(() => Promise.resolve(100));
|
|
(mockQueue.getFailedCount as any) = mock(() => Promise.resolve(3));
|
|
|
|
const report = await metrics.getReport();
|
|
|
|
expect(report).toContain('Queue Metrics Report');
|
|
expect(report).toContain('✅ Healthy');
|
|
expect(report).toContain('Waiting: 5');
|
|
expect(report).toContain('Active: 2');
|
|
expect(report).toContain('Completed: 100');
|
|
expect(report).toContain('Failed: 3');
|
|
});
|
|
});
|
|
|
|
describe('getPrometheusMetrics', () => {
|
|
it('should generate Prometheus formatted metrics', async () => {
|
|
(mockQueue.getWaitingCount as any) = mock(() => Promise.resolve(5));
|
|
(mockQueue.getActiveCount as any) = mock(() => Promise.resolve(2));
|
|
(mockQueue.getCompletedCount as any) = mock(() => Promise.resolve(100));
|
|
(mockQueue.getFailedCount as any) = mock(() => Promise.resolve(3));
|
|
|
|
const prometheusMetrics = await metrics.getPrometheusMetrics();
|
|
|
|
expect(prometheusMetrics).toContain('# HELP queue_jobs_total');
|
|
expect(prometheusMetrics).toContain('queue_jobs_total{queue="test-queue",status="waiting"} 5');
|
|
expect(prometheusMetrics).toContain('queue_jobs_total{queue="test-queue",status="active"} 2');
|
|
expect(prometheusMetrics).toContain('queue_jobs_total{queue="test-queue",status="completed"} 100');
|
|
expect(prometheusMetrics).toContain('# HELP queue_processing_time_seconds');
|
|
expect(prometheusMetrics).toContain('# HELP queue_throughput_per_minute');
|
|
expect(prometheusMetrics).toContain('# HELP queue_health');
|
|
});
|
|
});
|
|
|
|
describe('event listeners', () => {
|
|
it('should setup event listeners on construction', () => {
|
|
const newMockQueueEvents = {
|
|
on: mock(() => {}),
|
|
} as unknown as QueueEvents;
|
|
|
|
new QueueMetricsCollector(mockQueue, newMockQueueEvents);
|
|
|
|
expect(newMockQueueEvents.on).toHaveBeenCalledWith('completed', expect.any(Function));
|
|
expect(newMockQueueEvents.on).toHaveBeenCalledWith('failed', expect.any(Function));
|
|
expect(newMockQueueEvents.on).toHaveBeenCalledWith('active', expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe('oldest waiting job', () => {
|
|
it('should get oldest waiting job date', async () => {
|
|
const oldJob = {
|
|
timestamp: Date.now() - 60000, // 1 minute ago
|
|
};
|
|
|
|
(mockQueue.getWaiting as any) = mock(() => Promise.resolve([oldJob]));
|
|
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.oldestWaitingJob).toBeDefined();
|
|
expect(result.oldestWaitingJob).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should return null when no waiting jobs', async () => {
|
|
(mockQueue.getWaiting as any) = mock(() => Promise.resolve([]));
|
|
|
|
const result = await metrics.collect();
|
|
|
|
expect(result.oldestWaitingJob).toBeNull();
|
|
});
|
|
});
|
|
}) |