@ahmedhegazee/nestjs-telescope
Version:
Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling
623 lines • 26.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const testing_1 = require("@nestjs/testing");
const cache_watcher_service_1 = require("./cache-watcher.service");
const telescope_service_1 = require("../../core/services/telescope.service");
const cache_watcher_config_1 = require("./cache-watcher.config");
describe('CacheWatcherService', () => {
let service;
let telescopeService;
beforeEach(async () => {
const mockTelescopeService = {
record: jest.fn(),
};
const module = await testing_1.Test.createTestingModule({
providers: [
cache_watcher_service_1.CacheWatcherService,
{
provide: telescope_service_1.TelescopeService,
useValue: mockTelescopeService,
},
{
provide: 'CACHE_WATCHER_CONFIG',
useValue: cache_watcher_config_1.defaultCacheWatcherConfig,
},
],
}).compile();
service = module.get(cache_watcher_service_1.CacheWatcherService);
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.totalOperations).toBe(0);
expect(metrics.hitCount).toBe(0);
expect(metrics.missCount).toBe(0);
expect(metrics.healthScore).toBe(100);
});
it('should start periodic processing on module init', async () => {
await service.onModuleInit();
});
});
describe('cache operation tracking', () => {
it('should track cache hit', () => {
const context = {
id: 'cache-hit-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'user:123',
value: { id: 123, name: 'John' },
hit: true,
startTime: new Date(),
duration: 10,
};
service.trackCacheOperation(context);
expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({
type: 'cache',
content: expect.objectContaining({
cache: expect.objectContaining({
operation: 'get',
key: 'user:123',
hit: true,
}),
}),
}));
const metrics = service.getMetrics();
expect(metrics.totalOperations).toBe(1);
expect(metrics.hitCount).toBe(1);
expect(metrics.hitRate).toBe(100);
});
it('should track cache miss', () => {
const context = {
id: 'cache-miss-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'user:456',
hit: false,
startTime: new Date(),
duration: 5,
};
service.trackCacheOperation(context);
const metrics = service.getMetrics();
expect(metrics.totalOperations).toBe(1);
expect(metrics.missCount).toBe(1);
expect(metrics.missRate).toBe(100);
});
it('should track cache error', () => {
const context = {
id: 'cache-error-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.SET,
key: 'user:789',
value: { id: 789, name: 'Jane' },
hit: false,
startTime: new Date(),
duration: 50,
error: { message: 'Connection timeout', code: 'TIMEOUT' },
};
service.trackCacheOperation(context);
const metrics = service.getMetrics();
expect(metrics.totalOperations).toBe(1);
expect(metrics.errorCount).toBe(1);
expect(metrics.errorRate).toBe(100);
});
it('should not track operation if disabled', () => {
const disabledConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
enabled: false,
};
const disabledService = new cache_watcher_service_1.CacheWatcherService(telescopeService, disabledConfig);
const context = {
id: 'disabled-cache-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'test:key',
hit: true,
startTime: new Date(),
};
disabledService.trackCacheOperation(context);
expect(telescopeService.record).not.toHaveBeenCalled();
});
it('should respect sampling rate', () => {
const samplingConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
sampleRate: 0,
};
const samplingService = new cache_watcher_service_1.CacheWatcherService(telescopeService, samplingConfig);
const context = {
id: 'sampled-cache-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'test:key',
hit: true,
startTime: new Date(),
};
samplingService.trackCacheOperation(context);
expect(telescopeService.record).not.toHaveBeenCalled();
});
});
describe('filtering and exclusions', () => {
it('should exclude operations based on configuration', () => {
const excludeConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
excludeOperations: ['get'],
};
const excludeService = new cache_watcher_service_1.CacheWatcherService(telescopeService, excludeConfig);
const context = {
id: 'excluded-cache-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'test:key',
hit: true,
startTime: new Date(),
};
excludeService.trackCacheOperation(context);
expect(telescopeService.record).not.toHaveBeenCalled();
});
it('should exclude keys based on patterns', () => {
const excludeConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
excludeKeyPatterns: ['temp:*', 'debug:*'],
};
const excludeService = new cache_watcher_service_1.CacheWatcherService(telescopeService, excludeConfig);
const context = {
id: 'excluded-key-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'temp:session:123',
hit: true,
startTime: new Date(),
};
excludeService.trackCacheOperation(context);
expect(telescopeService.record).not.toHaveBeenCalled();
});
it('should include only specified key patterns', () => {
const includeConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
includeKeyPatterns: ['user:*'],
};
const includeService = new cache_watcher_service_1.CacheWatcherService(telescopeService, includeConfig);
const excludedContext = {
id: 'excluded-pattern-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'session:123',
hit: true,
startTime: new Date(),
};
includeService.trackCacheOperation(excludedContext);
expect(telescopeService.record).not.toHaveBeenCalled();
const includedContext = {
id: 'included-pattern-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'user:123',
hit: true,
startTime: new Date(),
};
includeService.trackCacheOperation(includedContext);
expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({
type: 'cache',
content: expect.objectContaining({
cache: expect.objectContaining({
key: 'user:123',
}),
}),
}));
});
});
describe('data sanitization', () => {
it('should sanitize sensitive keys', () => {
const context = {
id: 'sensitive-key-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'auth:token:abc123def456',
hit: true,
startTime: new Date(),
};
service.trackCacheOperation(context);
expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({
content: expect.objectContaining({
cache: expect.objectContaining({
key: expect.stringContaining('[HASH]'),
}),
}),
}));
});
it('should sanitize large values', () => {
const largeValue = 'x'.repeat(2000);
const context = {
id: 'large-value-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.SET,
key: 'large:data',
value: largeValue,
hit: false,
startTime: new Date(),
};
service.trackCacheOperation(context);
expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({
content: expect.objectContaining({
value: expect.objectContaining({
_truncated: true,
_size: expect.any(Number),
}),
}),
}));
});
it('should sanitize sensitive fields in values', () => {
const sensitiveValue = {
id: 123,
name: 'John',
password: 'secret123',
token: 'abc123def456',
};
const context = {
id: 'sensitive-value-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.SET,
key: 'user:123',
value: sensitiveValue,
hit: false,
startTime: new Date(),
};
const captureConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
captureValues: true,
};
const captureService = new cache_watcher_service_1.CacheWatcherService(telescopeService, captureConfig);
captureService.trackCacheOperation(context);
expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({
content: expect.objectContaining({
value: expect.objectContaining({
id: 123,
name: 'John',
password: '[REDACTED]',
token: '[REDACTED]',
}),
}),
}));
});
it('should limit key length', () => {
const longKey = 'very:long:key:' + 'x'.repeat(300);
const context = {
id: 'long-key-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: longKey,
hit: true,
startTime: new Date(),
};
service.trackCacheOperation(context);
expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({
content: expect.objectContaining({
cache: expect.objectContaining({
key: expect.stringMatching(/.*\.\.\.$/),
}),
}),
}));
});
});
describe('key pattern analysis', () => {
it('should extract and track key patterns', () => {
const keys = [
'user:123',
'user:456',
'user:789',
'session:abc123',
'session:def456',
];
keys.forEach((key, index) => {
const context = {
id: `pattern-${index}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key,
hit: index < 3,
startTime: new Date(),
duration: 10,
};
service.trackCacheOperation(context);
});
const metrics = service.getMetrics();
expect(metrics.topKeyPatterns).toHaveLength(2);
const userPattern = metrics.topKeyPatterns.find(p => p.pattern.includes('user'));
expect(userPattern).toBeDefined();
expect(userPattern.count).toBe(3);
expect(userPattern.hitRate).toBe(100);
const sessionPattern = metrics.topKeyPatterns.find(p => p.pattern.includes('session'));
expect(sessionPattern).toBeDefined();
expect(sessionPattern.count).toBe(2);
expect(sessionPattern.hitRate).toBe(0);
});
});
describe('metrics calculation', () => {
it('should calculate hit and miss rates correctly', () => {
const operations = [
{ hit: true, duration: 5 },
{ hit: true, duration: 8 },
{ hit: false, duration: 15 },
{ hit: false, duration: 20 },
{ hit: true, duration: 6 },
];
operations.forEach((op, index) => {
const context = {
id: `op-${index}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: `key:${index}`,
hit: op.hit,
startTime: new Date(),
duration: op.duration,
};
service.trackCacheOperation(context);
});
const metrics = service.getMetrics();
expect(metrics.totalOperations).toBe(5);
expect(metrics.hitCount).toBe(3);
expect(metrics.missCount).toBe(2);
expect(metrics.hitRate).toBe(60);
expect(metrics.missRate).toBe(40);
});
it('should track slow operations', () => {
const slowConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
slowOperationThreshold: 50,
};
const slowService = new cache_watcher_service_1.CacheWatcherService(telescopeService, slowConfig);
const context = {
id: 'slow-op-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'slow:key',
hit: true,
startTime: new Date(),
duration: 100,
};
slowService.trackCacheOperation(context);
const metrics = slowService.getMetrics();
expect(metrics.slowOperations).toBe(1);
});
it('should calculate average response time', () => {
const responseTimes = [10, 20, 30, 40, 50];
responseTimes.forEach((time, index) => {
const context = {
id: `response-time-${index}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: `key:${index}`,
hit: true,
startTime: new Date(),
duration: time,
};
service.trackCacheOperation(context);
});
const metrics = service.getMetrics();
expect(metrics.averageResponseTime).toBe(30);
});
});
describe('alerting system', () => {
it('should generate hit rate alerts', (done) => {
const alertConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
alertThresholds: {
hitRate: 80,
missRate: 20,
avgResponseTime: 100,
errorRate: 5,
memoryUsage: 80,
connectionCount: 100,
timeWindow: 300000,
},
};
const alertService = new cache_watcher_service_1.CacheWatcherService(telescopeService, alertConfig);
alertService.getAlertsStream().subscribe(alert => {
expect(alert.type).toBe('hit_rate');
expect(alert.severity).toBe('medium');
expect(alert.message).toContain('hit rate below threshold');
done();
});
for (let i = 0; i < 5; i++) {
const context = {
id: `low-hit-${i}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: `key:${i}`,
hit: i === 0,
startTime: new Date(),
duration: 10,
};
alertService.trackCacheOperation(context);
}
});
it('should generate slow operation alerts', (done) => {
const alertConfig = {
...cache_watcher_config_1.defaultCacheWatcherConfig,
alertThresholds: {
hitRate: 0,
missRate: 100,
avgResponseTime: 50,
errorRate: 100,
memoryUsage: 100,
connectionCount: 1000,
timeWindow: 300000,
},
};
const alertService = new cache_watcher_service_1.CacheWatcherService(telescopeService, alertConfig);
alertService.getAlertsStream().subscribe(alert => {
expect(alert.type).toBe('slow_operations');
expect(alert.severity).toBe('medium');
expect(alert.message).toContain('Slow cache operation');
done();
});
const context = {
id: 'slow-alert-1',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'slow:key',
hit: true,
startTime: new Date(),
duration: 100,
};
alertService.trackCacheOperation(context);
});
});
describe('health calculation', () => {
it('should calculate cache health correctly', () => {
const goodOperations = [
{ hit: true, duration: 5 },
{ hit: true, duration: 8 },
{ hit: true, duration: 6 },
{ hit: false, duration: 15 },
];
goodOperations.forEach((op, index) => {
const context = {
id: `health-${index}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: `key:${index}`,
hit: op.hit,
startTime: new Date(),
duration: op.duration,
cacheInstance: 'test-cache',
};
service.trackCacheOperation(context);
});
const health = service.getCacheHealth('test-cache');
expect(health).toBeDefined();
expect(Array.isArray(health)).toBe(false);
const cacheHealth = health;
expect(cacheHealth.instance).toBe('test-cache');
expect(cacheHealth.status).toBe('healthy');
expect(cacheHealth.score).toBeGreaterThan(70);
});
it('should return all cache instances health', () => {
const instances = ['cache-1', 'cache-2'];
instances.forEach((instance, index) => {
const context = {
id: `multi-health-${index}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: `key:${index}`,
hit: true,
startTime: new Date(),
duration: 10,
cacheInstance: instance,
};
service.trackCacheOperation(context);
});
const allHealth = service.getCacheHealth();
expect(Array.isArray(allHealth)).toBe(true);
expect(allHealth.length).toBe(2);
});
});
describe('public API', () => {
it('should provide metrics stream', (done) => {
const metricsStream = service.getMetricsStream();
metricsStream.subscribe(metrics => {
expect(metrics).toBeDefined();
expect(metrics.totalOperations).toBeGreaterThanOrEqual(0);
done();
});
const context = {
id: 'stream-test',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'stream:key',
hit: true,
startTime: new Date(),
};
service.trackCacheOperation(context);
});
it('should return recent operations', () => {
const operations = Array.from({ length: 5 }, (_, i) => ({
id: `recent-${i}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: `key:${i}`,
hit: true,
startTime: new Date(),
}));
operations.forEach(op => service.trackCacheOperation(op));
const recent = service.getRecentOperations(3);
expect(recent).toHaveLength(3);
expect(recent[0].id).toBe('recent-4');
});
it('should return operations by type', () => {
const operations = [
{ operation: cache_watcher_service_1.CacheOperation.GET },
{ operation: cache_watcher_service_1.CacheOperation.SET },
{ operation: cache_watcher_service_1.CacheOperation.GET },
];
operations.forEach((op, index) => {
const context = {
id: `type-${index}`,
timestamp: new Date(),
operation: op.operation,
key: `key:${index}`,
hit: true,
startTime: new Date(),
};
service.trackCacheOperation(context);
});
const getOps = service.getOperationsByType(cache_watcher_service_1.CacheOperation.GET);
expect(getOps).toHaveLength(2);
expect(getOps.every(op => op.operation === cache_watcher_service_1.CacheOperation.GET)).toBe(true);
});
it('should acknowledge alerts', () => {
const alerts = [];
service.getAlertsStream().subscribe(alert => alerts.push(alert));
const context = {
id: 'alert-test',
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: 'alert:key',
hit: true,
startTime: new Date(),
duration: 200,
};
service.trackCacheOperation(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: `cleanup-${i}`,
timestamp: new Date(),
operation: cache_watcher_service_1.CacheOperation.GET,
key: `key:${i}`,
hit: true,
startTime: new Date(),
};
service.trackCacheOperation(context);
}
const history = service.cacheHistory;
expect(history.length).toBeLessThanOrEqual(maxSize);
});
});
});
//# sourceMappingURL=cache-watcher.service.spec.js.map