295 lines
11 KiB
JavaScript
295 lines
11 KiB
JavaScript
// resource-monitor.js creates a Docker instance at module level.
|
|
// On test machines without Docker, the constructor reads from non-existent files (returns defaults).
|
|
|
|
const resourceMonitor = require('../resource-monitor');
|
|
|
|
beforeEach(() => {
|
|
// Reset singleton state
|
|
resourceMonitor.stats = new Map();
|
|
resourceMonitor.alerts = new Map();
|
|
resourceMonitor.lastAlerts = new Map();
|
|
resourceMonitor.monitoring = false;
|
|
if (resourceMonitor.monitoringInterval) {
|
|
clearInterval(resourceMonitor.monitoringInterval);
|
|
resourceMonitor.monitoringInterval = null;
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
resourceMonitor.stop();
|
|
});
|
|
|
|
// Helper: create a stat entry
|
|
function makeStat(cpu = 10, memory = 50, timestamp = new Date().toISOString()) {
|
|
return {
|
|
timestamp,
|
|
cpu: { percent: cpu, usage: cpu * 1000 },
|
|
memory: { usage: memory * 1024 * 1024, limit: 1024 * 1024 * 1024, percent: memory, usageMB: memory, limitMB: 1024 },
|
|
network: { rxBytes: 0, txBytes: 0, rxMB: 0, txMB: 0 },
|
|
disk: { readBytes: 0, writeBytes: 0, readMB: 0, writeMB: 0 },
|
|
pids: 5,
|
|
};
|
|
}
|
|
|
|
describe('recordStats', () => {
|
|
test('creates new entry for unknown container', () => {
|
|
resourceMonitor.recordStats('c1', '/my-app', makeStat());
|
|
expect(resourceMonitor.stats.has('c1')).toBe(true);
|
|
expect(resourceMonitor.stats.get('c1').history).toHaveLength(1);
|
|
});
|
|
|
|
test('appends to existing history', () => {
|
|
resourceMonitor.recordStats('c1', '/my-app', makeStat());
|
|
resourceMonitor.recordStats('c1', '/my-app', makeStat());
|
|
expect(resourceMonitor.stats.get('c1').history).toHaveLength(2);
|
|
});
|
|
|
|
test('updates container name', () => {
|
|
resourceMonitor.recordStats('c1', '/old-name', makeStat());
|
|
resourceMonitor.recordStats('c1', '/new-name', makeStat());
|
|
expect(resourceMonitor.stats.get('c1').name).toBe('/new-name');
|
|
});
|
|
});
|
|
|
|
describe('getCurrentStats', () => {
|
|
test('returns null for unknown container', () => {
|
|
expect(resourceMonitor.getCurrentStats('nonexistent')).toBeNull();
|
|
});
|
|
|
|
test('returns latest history entry', () => {
|
|
const stat1 = makeStat(10);
|
|
const stat2 = makeStat(50);
|
|
resourceMonitor.recordStats('c1', '/app', stat1);
|
|
resourceMonitor.recordStats('c1', '/app', stat2);
|
|
expect(resourceMonitor.getCurrentStats('c1').cpu.percent).toBe(50);
|
|
});
|
|
});
|
|
|
|
describe('getHistoricalStats', () => {
|
|
test('returns empty array for unknown container', () => {
|
|
expect(resourceMonitor.getHistoricalStats('nonexistent')).toEqual([]);
|
|
});
|
|
|
|
test('filters by time window', () => {
|
|
const recent = makeStat(10, 50, new Date().toISOString());
|
|
const old = makeStat(10, 50, new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString());
|
|
resourceMonitor.stats.set('c1', { name: '/app', history: [old, recent] });
|
|
const result = resourceMonitor.getHistoricalStats('c1', 24);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toBe(recent);
|
|
});
|
|
});
|
|
|
|
describe('getAggregatedStats', () => {
|
|
test('returns null for unknown container', () => {
|
|
expect(resourceMonitor.getAggregatedStats('nonexistent')).toBeNull();
|
|
});
|
|
|
|
test('returns null when no recent history', () => {
|
|
const old = makeStat(10, 50, new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString());
|
|
resourceMonitor.stats.set('c1', { name: '/app', history: [old] });
|
|
expect(resourceMonitor.getAggregatedStats('c1', 24)).toBeNull();
|
|
});
|
|
|
|
test('calculates correct avg/min/max for CPU', () => {
|
|
const now = new Date().toISOString();
|
|
resourceMonitor.stats.set('c1', {
|
|
name: '/app',
|
|
history: [makeStat(10, 50, now), makeStat(30, 50, now), makeStat(20, 50, now)],
|
|
});
|
|
const agg = resourceMonitor.getAggregatedStats('c1', 24);
|
|
expect(agg.cpu.avg).toBe(20);
|
|
expect(agg.cpu.min).toBe(10);
|
|
expect(agg.cpu.max).toBe(30);
|
|
});
|
|
|
|
test('calculates correct avg/min/max for memory', () => {
|
|
const now = new Date().toISOString();
|
|
resourceMonitor.stats.set('c1', {
|
|
name: '/app',
|
|
history: [makeStat(10, 40, now), makeStat(10, 60, now), makeStat(10, 80, now)],
|
|
});
|
|
const agg = resourceMonitor.getAggregatedStats('c1', 24);
|
|
expect(agg.memory.avg).toBe(60);
|
|
expect(agg.memory.min).toBe(40);
|
|
expect(agg.memory.max).toBe(80);
|
|
});
|
|
|
|
test('includes dataPoints and timeRange', () => {
|
|
const now = new Date().toISOString();
|
|
resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] });
|
|
const agg = resourceMonitor.getAggregatedStats('c1', 24);
|
|
expect(agg.dataPoints).toBe(1);
|
|
expect(agg.timeRange).toBe(24);
|
|
});
|
|
});
|
|
|
|
describe('checkAlerts', () => {
|
|
test('does nothing when alert config is missing', () => {
|
|
const handler = jest.fn();
|
|
resourceMonitor.on('alert', handler);
|
|
resourceMonitor.checkAlerts('c1', '/app', makeStat(99));
|
|
expect(handler).not.toHaveBeenCalled();
|
|
resourceMonitor.removeListener('alert', handler);
|
|
});
|
|
|
|
test('does nothing when alerts are disabled', () => {
|
|
resourceMonitor.alerts.set('c1', { enabled: false, cpuThreshold: 50 });
|
|
const handler = jest.fn();
|
|
resourceMonitor.on('alert', handler);
|
|
resourceMonitor.checkAlerts('c1', '/app', makeStat(99));
|
|
expect(handler).not.toHaveBeenCalled();
|
|
resourceMonitor.removeListener('alert', handler);
|
|
});
|
|
|
|
test('triggers CPU alert when threshold exceeded', () => {
|
|
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 50, cooldownMinutes: 0 });
|
|
const handler = jest.fn();
|
|
resourceMonitor.on('alert', handler);
|
|
resourceMonitor.checkAlerts('c1', '/app', makeStat(75));
|
|
expect(handler).toHaveBeenCalled();
|
|
const alertData = handler.mock.calls[0][0];
|
|
expect(alertData.alerts[0].type).toBe('cpu');
|
|
resourceMonitor.removeListener('alert', handler);
|
|
});
|
|
|
|
test('triggers memory alert when threshold exceeded', () => {
|
|
resourceMonitor.alerts.set('c1', { enabled: true, memoryThreshold: 70, cooldownMinutes: 0 });
|
|
const handler = jest.fn();
|
|
resourceMonitor.on('alert', handler);
|
|
resourceMonitor.checkAlerts('c1', '/app', makeStat(10, 80));
|
|
expect(handler).toHaveBeenCalled();
|
|
const alertData = handler.mock.calls[0][0];
|
|
expect(alertData.alerts[0].type).toBe('memory');
|
|
resourceMonitor.removeListener('alert', handler);
|
|
});
|
|
|
|
test('respects cooldown period', () => {
|
|
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 50, cooldownMinutes: 15 });
|
|
resourceMonitor.lastAlerts.set('c1', Date.now()); // Just alerted
|
|
const handler = jest.fn();
|
|
resourceMonitor.on('alert', handler);
|
|
resourceMonitor.checkAlerts('c1', '/app', makeStat(99));
|
|
expect(handler).not.toHaveBeenCalled();
|
|
resourceMonitor.removeListener('alert', handler);
|
|
});
|
|
|
|
test('does not trigger when below threshold', () => {
|
|
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 90, cooldownMinutes: 0 });
|
|
const handler = jest.fn();
|
|
resourceMonitor.on('alert', handler);
|
|
resourceMonitor.checkAlerts('c1', '/app', makeStat(50));
|
|
expect(handler).not.toHaveBeenCalled();
|
|
resourceMonitor.removeListener('alert', handler);
|
|
});
|
|
});
|
|
|
|
describe('setAlertConfig / getAlertConfig / removeAlertConfig', () => {
|
|
test('stores alert config', () => {
|
|
resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 });
|
|
expect(resourceMonitor.alerts.has('c1')).toBe(true);
|
|
});
|
|
|
|
test('retrieves stored config', () => {
|
|
resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 });
|
|
const config = resourceMonitor.getAlertConfig('c1');
|
|
expect(config.cpuThreshold).toBe(80);
|
|
});
|
|
|
|
test('returns null for non-existent config', () => {
|
|
expect(resourceMonitor.getAlertConfig('nonexistent')).toBeNull();
|
|
});
|
|
|
|
test('removes config and last alert', () => {
|
|
resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 });
|
|
resourceMonitor.lastAlerts.set('c1', Date.now());
|
|
resourceMonitor.removeAlertConfig('c1');
|
|
expect(resourceMonitor.alerts.has('c1')).toBe(false);
|
|
expect(resourceMonitor.lastAlerts.has('c1')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getAllStats', () => {
|
|
test('returns empty object when no stats', () => {
|
|
expect(resourceMonitor.getAllStats()).toEqual({});
|
|
});
|
|
|
|
test('includes current and aggregated for each container', () => {
|
|
const now = new Date().toISOString();
|
|
resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] });
|
|
const all = resourceMonitor.getAllStats();
|
|
expect(all['c1']).toBeDefined();
|
|
expect(all['c1'].name).toBe('/app');
|
|
expect(all['c1'].current).toBeDefined();
|
|
expect(all['c1'].aggregated).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('exportStats / importStats', () => {
|
|
test('export returns object with stats and alerts', () => {
|
|
const now = new Date().toISOString();
|
|
resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] });
|
|
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 80 });
|
|
const exported = resourceMonitor.exportStats();
|
|
expect(exported.stats).toBeDefined();
|
|
expect(exported.alerts).toBeDefined();
|
|
expect(exported.exportedAt).toBeDefined();
|
|
});
|
|
|
|
test('import restores stats from backup', () => {
|
|
const backup = {
|
|
stats: { 'c1': { name: '/app', history: [makeStat()] } },
|
|
alerts: { 'c1': { enabled: true, cpuThreshold: 80 } },
|
|
};
|
|
resourceMonitor.importStats(backup);
|
|
expect(resourceMonitor.stats.has('c1')).toBe(true);
|
|
expect(resourceMonitor.alerts.has('c1')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('cleanupOldStats', () => {
|
|
test('removes entries older than retention period', () => {
|
|
const old = makeStat(10, 50, new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString());
|
|
const recent = makeStat(10, 50, new Date().toISOString());
|
|
resourceMonitor.stats.set('c1', { name: '/app', history: [old, recent] });
|
|
resourceMonitor.cleanupOldStats();
|
|
expect(resourceMonitor.stats.get('c1').history).toHaveLength(1);
|
|
});
|
|
|
|
test('deletes container entirely when no recent data', () => {
|
|
const old = makeStat(10, 50, new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString());
|
|
resourceMonitor.stats.set('c1', { name: '/app', history: [old] });
|
|
resourceMonitor.cleanupOldStats();
|
|
expect(resourceMonitor.stats.has('c1')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('start / stop', () => {
|
|
test('start sets monitoring flag', () => {
|
|
jest.useFakeTimers();
|
|
resourceMonitor.start();
|
|
expect(resourceMonitor.monitoring).toBe(true);
|
|
resourceMonitor.stop();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
test('stop clears interval', () => {
|
|
jest.useFakeTimers();
|
|
resourceMonitor.start();
|
|
resourceMonitor.stop();
|
|
expect(resourceMonitor.monitoring).toBe(false);
|
|
expect(resourceMonitor.monitoringInterval).toBeNull();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
test('start is idempotent', () => {
|
|
jest.useFakeTimers();
|
|
resourceMonitor.start();
|
|
const first = resourceMonitor.monitoringInterval;
|
|
resourceMonitor.start();
|
|
expect(resourceMonitor.monitoringInterval).toBe(first);
|
|
resourceMonitor.stop();
|
|
jest.useRealTimers();
|
|
});
|
|
});
|