@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
JavaScript
;
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