UNPKG

@ahmedhegazee/nestjs-telescope

Version:

Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling

463 lines 19.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const job_watcher_service_1 = require("./job-watcher.service"); const telescope_service_1 = require("../../core/services/telescope.service"); const job_watcher_config_1 = require("./job-watcher.config"); describe('JobWatcherService', () => { let service; let telescopeService; beforeEach(async () => { const mockTelescopeService = { record: jest.fn(), }; const module = await testing_1.Test.createTestingModule({ providers: [ job_watcher_service_1.JobWatcherService, { provide: telescope_service_1.TelescopeService, useValue: mockTelescopeService, }, { provide: 'JOB_WATCHER_CONFIG', useValue: job_watcher_config_1.defaultJobWatcherConfig, }, ], }).compile(); service = module.get(job_watcher_service_1.JobWatcherService); telescopeService = module.get(telescope_service_1.TelescopeService); }); afterEach(() => { jest.clearAllMocks(); }); describe('initialization', () => { it('should be defined', () => { expect(service).toBeDefined(); }); it('should initialize with default metrics', () => { const metrics = service.getMetrics(); expect(metrics.totalJobs).toBe(0); expect(metrics.completedJobs).toBe(0); expect(metrics.failedJobs).toBe(0); expect(metrics.healthScore).toBe(100); }); it('should start periodic processing on module init', async () => { await service.onModuleInit(); }); }); describe('job tracking', () => { it('should track job completion', () => { const context = { id: 'test-job-1', jobId: 'job-123', queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, duration: 1000, result: { success: true }, }; service.trackJob(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ type: 'job', content: expect.objectContaining({ job: expect.objectContaining({ id: 'job-123', name: 'test-job', queue: 'test-queue', status: 'completed', }), }), })); const metrics = service.getMetrics(); expect(metrics.totalJobs).toBe(1); expect(metrics.completedJobs).toBe(1); }); it('should track job failure', () => { const context = { id: 'test-job-2', jobId: 'job-456', queueName: 'test-queue', jobName: 'failing-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.FAILED, priority: 1, attempts: 3, maxAttempts: 3, duration: 500, error: { message: 'Job failed', code: 'PROCESSING_ERROR' }, }; service.trackJob(context); const metrics = service.getMetrics(); expect(metrics.totalJobs).toBe(1); expect(metrics.failedJobs).toBe(1); expect(metrics.failureRate).toBeGreaterThan(0); }); it('should not track job if disabled', () => { const disabledConfig = { ...job_watcher_config_1.defaultJobWatcherConfig, enabled: false, }; const disabledService = new job_watcher_service_1.JobWatcherService(telescopeService, disabledConfig); const context = { id: 'test-job-3', jobId: 'job-789', queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, }; disabledService.trackJob(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); it('should respect sampling rate', () => { const samplingConfig = { ...job_watcher_config_1.defaultJobWatcherConfig, sampleRate: 0, }; const samplingService = new job_watcher_service_1.JobWatcherService(telescopeService, samplingConfig); const context = { id: 'test-job-4', jobId: 'job-000', queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, }; samplingService.trackJob(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); it('should exclude jobs based on configuration', () => { const excludeConfig = { ...job_watcher_config_1.defaultJobWatcherConfig, excludeJobTypes: ['test-job'], }; const excludeService = new job_watcher_service_1.JobWatcherService(telescopeService, excludeConfig); const context = { id: 'test-job-5', jobId: 'job-111', queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, }; excludeService.trackJob(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); }); describe('metrics calculation', () => { it('should calculate job rates correctly', () => { const jobs = [ { status: job_watcher_service_1.JobStatus.COMPLETED, duration: 1000 }, { status: job_watcher_service_1.JobStatus.COMPLETED, duration: 2000 }, { status: job_watcher_service_1.JobStatus.FAILED, duration: 500 }, { status: job_watcher_service_1.JobStatus.ACTIVE, duration: undefined }, ]; jobs.forEach((job, index) => { const context = { id: `job-${index}`, jobId: `job-${index}`, queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job.status, priority: 1, attempts: 1, maxAttempts: 3, duration: job.duration, }; service.trackJob(context); }); const metrics = service.getMetrics(); expect(metrics.totalJobs).toBe(4); expect(metrics.completedJobs).toBe(2); expect(metrics.failedJobs).toBe(1); expect(metrics.activeJobs).toBe(1); expect(metrics.failureRate).toBe(25); }); it('should track slow jobs', () => { const slowConfig = { ...job_watcher_config_1.defaultJobWatcherConfig, slowJobThreshold: 1000, }; const slowService = new job_watcher_service_1.JobWatcherService(telescopeService, slowConfig); const context = { id: 'slow-job-1', jobId: 'slow-job-1', queueName: 'test-queue', jobName: 'slow-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, duration: 5000, }; slowService.trackJob(context); const metrics = slowService.getMetrics(); expect(metrics.slowJobs).toBe(1); }); it('should calculate top failed jobs', () => { for (let i = 0; i < 5; i++) { const context = { id: `failing-job-${i}`, jobId: `failing-job-${i}`, queueName: 'test-queue', jobName: 'failing-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.FAILED, priority: 1, attempts: 3, maxAttempts: 3, error: { message: 'Consistent failure' }, }; service.trackJob(context); } const metrics = service.getMetrics(); expect(metrics.topFailedJobs).toHaveLength(1); expect(metrics.topFailedJobs[0].jobName).toBe('failing-job'); expect(metrics.topFailedJobs[0].failureCount).toBe(5); }); }); describe('alerting system', () => { it('should generate failure rate alerts', (done) => { const alertConfig = { ...job_watcher_config_1.defaultJobWatcherConfig, alertThresholds: { failureRate: 20, avgExecutionTime: 10000, queueSize: 1000, stalledJobs: 5, timeWindow: 300000, }, }; const alertService = new job_watcher_service_1.JobWatcherService(telescopeService, alertConfig); alertService.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('failure_rate'); expect(alert.severity).toBe('high'); expect(alert.message).toContain('failure rate exceeded threshold'); done(); }); for (let i = 0; i < 10; i++) { const context = { id: `job-${i}`, jobId: `job-${i}`, queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: i < 3 ? job_watcher_service_1.JobStatus.FAILED : job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, }; alertService.trackJob(context); } }); it('should generate slow job alerts', (done) => { const alertConfig = { ...job_watcher_config_1.defaultJobWatcherConfig, alertThresholds: { failureRate: 100, avgExecutionTime: 1000, queueSize: 1000, stalledJobs: 5, timeWindow: 300000, }, }; const alertService = new job_watcher_service_1.JobWatcherService(telescopeService, alertConfig); alertService.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('slow_jobs'); expect(alert.severity).toBe('medium'); expect(alert.message).toContain('Slow job detected'); done(); }); const context = { id: 'slow-job-alert', jobId: 'slow-job-alert', queueName: 'test-queue', jobName: 'slow-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, duration: 5000, }; alertService.trackJob(context); }); }); describe('public API', () => { it('should provide metrics stream', (done) => { const metricsStream = service.getMetricsStream(); metricsStream.subscribe(metrics => { expect(metrics).toBeDefined(); expect(metrics.totalJobs).toBeGreaterThanOrEqual(0); done(); }); const context = { id: 'test-job-stream', jobId: 'test-job-stream', queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, }; service.trackJob(context); }); it('should return recent jobs', () => { const contexts = Array.from({ length: 5 }, (_, i) => ({ id: `job-${i}`, jobId: `job-${i}`, queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, })); contexts.forEach(context => service.trackJob(context)); const recent = service.getRecentJobs(3); expect(recent).toHaveLength(3); expect(recent[0].id).toBe('job-4'); }); it('should return jobs by queue', () => { const queues = ['queue-a', 'queue-b', 'queue-a']; queues.forEach((queue, index) => { const context = { id: `job-${index}`, jobId: `job-${index}`, queueName: queue, jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, }; service.trackJob(context); }); const queueAJobs = service.getJobsByQueue('queue-a'); expect(queueAJobs).toHaveLength(2); expect(queueAJobs.every(job => job.queueName === 'queue-a')).toBe(true); }); it('should return jobs by status', () => { const statuses = [job_watcher_service_1.JobStatus.COMPLETED, job_watcher_service_1.JobStatus.FAILED, job_watcher_service_1.JobStatus.COMPLETED]; statuses.forEach((status, index) => { const context = { id: `job-${index}`, jobId: `job-${index}`, queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status, priority: 1, attempts: 1, maxAttempts: 3, }; service.trackJob(context); }); const completedJobs = service.getJobsByStatus(job_watcher_service_1.JobStatus.COMPLETED); expect(completedJobs).toHaveLength(2); expect(completedJobs.every(job => job.status === job_watcher_service_1.JobStatus.COMPLETED)).toBe(true); }); it('should calculate queue health', () => { const jobs = [ { status: job_watcher_service_1.JobStatus.COMPLETED, duration: 1000 }, { status: job_watcher_service_1.JobStatus.COMPLETED, duration: 2000 }, { status: job_watcher_service_1.JobStatus.FAILED, duration: 500 }, ]; jobs.forEach((job, index) => { const context = { id: `job-${index}`, jobId: `job-${index}`, queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job.status, priority: 1, attempts: 1, maxAttempts: 3, duration: job.duration, }; service.trackJob(context); }); const health = service.getQueueHealth('test-queue'); expect(health).toBeDefined(); expect(Array.isArray(health)).toBe(false); const queueHealth = health; expect(queueHealth.queueName).toBe('test-queue'); expect(queueHealth.status).toBeDefined(); expect(queueHealth.score).toBeGreaterThan(0); }); it('should acknowledge alerts', () => { const alerts = []; service.getAlertsStream().subscribe(alert => alerts.push(alert)); const context = { id: 'alert-job', jobId: 'alert-job', queueName: 'test-queue', jobName: 'slow-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, duration: 15000, }; service.trackJob(context); setTimeout(() => { if (alerts.length > 0) { const alert = alerts[0]; const acknowledged = service.acknowledgeAlert(alert.id); expect(acknowledged).toBe(true); expect(alert.acknowledged).toBe(true); } }, 100); }); }); describe('cleanup and resource management', () => { it('should cleanup on destroy', () => { const destroySpy = jest.spyOn(service.destroy$, 'next'); const completeSpy = jest.spyOn(service.destroy$, 'complete'); service.onModuleDestroy(); expect(destroySpy).toHaveBeenCalled(); expect(completeSpy).toHaveBeenCalled(); }); it('should limit history size', () => { const maxSize = service.maxHistorySize; for (let i = 0; i < maxSize + 100; i++) { const context = { id: `job-${i}`, jobId: `job-${i}`, queueName: 'test-queue', jobName: 'test-job', timestamp: new Date(), status: job_watcher_service_1.JobStatus.COMPLETED, priority: 1, attempts: 1, maxAttempts: 3, }; service.trackJob(context); } const history = service.jobHistory; expect(history.length).toBeLessThanOrEqual(maxSize); }); }); }); //# sourceMappingURL=job-watcher.service.spec.js.map