After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:
1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths
Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.
Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)
Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
287 lines
11 KiB
JavaScript
287 lines
11 KiB
JavaScript
const express = require('express');
|
|
const { success } = require('../src/utils/responses');
|
|
|
|
/**
|
|
* Monitoring routes factory
|
|
* @param {Object} deps - Explicit dependencies
|
|
* @param {Object} deps.resourceMonitor - Resource monitoring manager
|
|
* @param {Object} deps.docker - Docker client wrapper
|
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
|
* @param {Object} deps.log - Logger instance
|
|
* @returns {express.Router}
|
|
*/
|
|
module.exports = function({ resourceMonitor, docker, asyncHandler, log, notificationManager }) {
|
|
const router = express.Router();
|
|
|
|
// ===== RESOURCE MONITORING ENDPOINTS =====
|
|
|
|
// Get all container stats (from resource monitor module)
|
|
// Returns a flat summary format for the System Overview widget:
|
|
// { containerId: { cpu: <percent>, memory: <percent>, memoryUsage: <bytes>, name } }
|
|
router.get('/monitoring/stats', asyncHandler(async (req, res) => {
|
|
const raw = resourceMonitor.getAllStats();
|
|
// Transform nested { current: { cpu: { percent }, memory: { percent, usage } } }
|
|
// into flat { cpu: number, memory: number, memoryUsage: number } for the frontend widget
|
|
const stats = {};
|
|
for (const [id, data] of Object.entries(raw)) {
|
|
const cur = data.current || {};
|
|
stats[id] = {
|
|
name: data.name,
|
|
cpu: typeof cur.cpu === 'object' ? (cur.cpu.percent ?? 0) : (Number(cur.cpu) || 0),
|
|
memory: typeof cur.memory === 'object' ? (cur.memory.percent ?? 0) : (Number(cur.memory) || 0),
|
|
memoryUsage: typeof cur.memory === 'object' ? (cur.memory.usage ?? 0) : 0,
|
|
};
|
|
}
|
|
success(res, { stats });
|
|
}, 'monitoring-stats'));
|
|
|
|
// Get stats for specific container
|
|
router.get('/monitoring/stats/:containerId', asyncHandler(async (req, res) => {
|
|
const stats = resourceMonitor.getCurrentStats(req.params.containerId);
|
|
if (!stats) {
|
|
const { NotFoundError } = require('../src/utilities/errors');
|
|
throw new NotFoundError('Container');
|
|
}
|
|
success(res, { stats });
|
|
}, 'monitoring-stats-container'));
|
|
|
|
// Get historical stats — supports either ?hours=24 (legacy raw) OR ?startTime=...&endTime=...
|
|
// (range mode auto-selects raw / hourly / daily tier)
|
|
router.get('/monitoring/history/:containerId', asyncHandler(async (req, res) => {
|
|
const containerId = req.params.containerId;
|
|
|
|
// Range mode (preferred)
|
|
if (req.query.startTime && req.query.endTime) {
|
|
const startTime = parseInt(req.query.startTime, 10);
|
|
const endTime = parseInt(req.query.endTime, 10);
|
|
if (Number.isNaN(startTime) || Number.isNaN(endTime) || startTime >= endTime) {
|
|
const { ValidationError } = require('../src/utilities/errors');
|
|
throw new ValidationError('Invalid startTime/endTime');
|
|
}
|
|
const result = resourceMonitor.getHistoryByRange(containerId, startTime, endTime);
|
|
success(res, { ...result, startTime, endTime });
|
|
return;
|
|
}
|
|
|
|
// Legacy hours-based mode (raw samples only)
|
|
const hours = parseInt(req.query.hours) || 24;
|
|
const history = resourceMonitor.getHistoricalStats(containerId, hours);
|
|
success(res, { history, hours, tier: 'raw', samples: history, unit: '10s' });
|
|
}, 'monitoring-history'));
|
|
|
|
// Get aggregated stats
|
|
router.get('/monitoring/aggregated/:containerId', asyncHandler(async (req, res) => {
|
|
const hours = parseInt(req.query.hours) || 24;
|
|
const aggregated = resourceMonitor.getAggregatedStats(req.params.containerId, hours);
|
|
if (!aggregated) {
|
|
const { NotFoundError } = require('../src/utilities/errors');
|
|
throw new NotFoundError('Monitoring data');
|
|
}
|
|
success(res, { aggregated, hours });
|
|
}, 'monitoring-aggregated'));
|
|
|
|
// ===== ALERT CONFIGURATION (bulk) =====
|
|
|
|
// Get all alert configs
|
|
router.get('/monitoring/alerts/config', asyncHandler(async (req, res) => {
|
|
const configs = resourceMonitor.getAllAlertConfigs();
|
|
success(res, { configs });
|
|
}, 'monitoring-alerts-config-get'));
|
|
|
|
// Set all alert configs (bulk update)
|
|
router.post('/monitoring/alerts/config', asyncHandler(async (req, res) => {
|
|
const { configs } = req.body;
|
|
if (!configs || typeof configs !== 'object') {
|
|
const { ValidationError } = require('../src/utilities/errors');
|
|
throw new ValidationError('configs object required');
|
|
}
|
|
for (const [containerId, config] of Object.entries(configs)) {
|
|
resourceMonitor.setAlertConfig(containerId, config);
|
|
}
|
|
success(res, { message: 'Alert configurations saved' });
|
|
}, 'monitoring-alerts-config-set'));
|
|
|
|
// Get alert history
|
|
router.get('/monitoring/alerts', asyncHandler(async (req, res) => {
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const history = resourceMonitor.getAlertHistory(limit);
|
|
success(res, { history });
|
|
}, 'monitoring-alerts-history'));
|
|
|
|
// Send test alert notification for a container
|
|
router.post('/monitoring/alerts/:containerId/test', asyncHandler(async (req, res) => {
|
|
const { containerId } = req.params;
|
|
|
|
// Get container name from docker
|
|
let containerName = containerId;
|
|
try {
|
|
const containers = await docker.client.listContainers({ all: false });
|
|
const containerInfo = containers.find(c => c.Id === containerId || c.Id.startsWith(containerId));
|
|
if (containerInfo) {
|
|
containerName = containerInfo.Names[0]?.replace(/^\//, '') || containerId;
|
|
}
|
|
} catch (_) {}
|
|
|
|
const testAlert = {
|
|
containerId,
|
|
containerName,
|
|
timestamp: new Date().toISOString(),
|
|
alerts: [{
|
|
type: 'test',
|
|
severity: 'info',
|
|
message: 'This is a test alert notification',
|
|
value: 0,
|
|
threshold: 0
|
|
}],
|
|
stats: null,
|
|
config: resourceMonitor.getAlertConfig(containerId) || {}
|
|
};
|
|
|
|
if (notificationManager) {
|
|
await notificationManager.sendAlert(testAlert);
|
|
}
|
|
|
|
// Also log to alert history
|
|
resourceMonitor.addAlertHistoryEntry({
|
|
id: `test-${Date.now()}`,
|
|
timestamp: new Date().toISOString(),
|
|
containerId,
|
|
containerName,
|
|
type: 'test',
|
|
metric: 'test',
|
|
value: 0,
|
|
threshold: 0,
|
|
severity: 'info',
|
|
notified: true,
|
|
autoRestartTriggered: false
|
|
});
|
|
|
|
success(res, { message: 'Test alert sent', alert: testAlert });
|
|
}, 'monitoring-alerts-test'));
|
|
|
|
// Configure alerts for a container
|
|
router.post('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
|
|
resourceMonitor.setAlertConfig(req.params.containerId, req.body);
|
|
success(res, { message: 'Alert configuration saved' });
|
|
}, 'monitoring-alerts-set'));
|
|
|
|
// Get alert configuration
|
|
router.get('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
|
|
const config = resourceMonitor.getAlertConfig(req.params.containerId);
|
|
success(res, { config: config || {} });
|
|
}, 'monitoring-alerts-get'));
|
|
|
|
// Delete alert configuration
|
|
router.delete('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
|
|
resourceMonitor.removeAlertConfig(req.params.containerId);
|
|
success(res, { message: 'Alert configuration removed' });
|
|
}, 'monitoring-alerts-delete'));
|
|
|
|
// ===== CONTAINER STATS ENDPOINTS (legacy /stats/) =====
|
|
|
|
// Get all container stats (live Docker stats)
|
|
router.get('/stats/containers', asyncHandler(async (req, res) => {
|
|
const containers = await docker.client.listContainers({ all: false });
|
|
const stats = [];
|
|
|
|
for (const containerInfo of containers) {
|
|
try {
|
|
const container = docker.client.getContainer(containerInfo.Id);
|
|
const containerStats = await container.stats({ stream: false });
|
|
|
|
// Calculate CPU percentage
|
|
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
|
|
(containerStats.precpu_stats.cpu_usage?.total_usage || 0);
|
|
const systemDelta = containerStats.cpu_stats.system_cpu_usage -
|
|
(containerStats.precpu_stats.system_cpu_usage || 0);
|
|
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0;
|
|
|
|
// Calculate memory usage
|
|
const memUsage = containerStats.memory_stats.usage || 0;
|
|
const memLimit = containerStats.memory_stats.limit || 1;
|
|
const memPercent = (memUsage / memLimit) * 100;
|
|
|
|
// Network stats
|
|
let netRx = 0, netTx = 0;
|
|
if (containerStats.networks) {
|
|
for (const net of Object.values(containerStats.networks)) {
|
|
netRx += net.rx_bytes || 0;
|
|
netTx += net.tx_bytes || 0;
|
|
}
|
|
}
|
|
|
|
stats.push({
|
|
id: containerInfo.Id.slice(0, 12),
|
|
name: containerInfo.Names[0]?.replace(/^\//, '') || 'unknown',
|
|
image: containerInfo.Image,
|
|
status: containerInfo.State,
|
|
cpu: {
|
|
percent: Math.round(cpuPercent * 100) / 100
|
|
},
|
|
memory: {
|
|
used: memUsage,
|
|
limit: memLimit,
|
|
percent: Math.round(memPercent * 100) / 100
|
|
},
|
|
network: {
|
|
rx: netRx,
|
|
tx: netTx
|
|
}
|
|
});
|
|
} catch (e) {
|
|
// Skip containers we can't get stats for
|
|
log.warn('monitoring', `Could not get stats for ${containerInfo.Names[0]}`, { error: e.message });
|
|
}
|
|
}
|
|
|
|
success(res, { stats, timestamp: new Date().toISOString() });
|
|
}, 'stats-containers'));
|
|
|
|
// Get single container stats
|
|
router.get('/stats/container/:id', asyncHandler(async (req, res) => {
|
|
const container = docker.client.getContainer(req.params.id);
|
|
const containerStats = await container.stats({ stream: false });
|
|
const info = await container.inspect();
|
|
|
|
// Calculate CPU percentage
|
|
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
|
|
(containerStats.precpu_stats.cpu_usage?.total_usage || 0);
|
|
const systemDelta = containerStats.cpu_stats.system_cpu_usage -
|
|
(containerStats.precpu_stats.system_cpu_usage || 0);
|
|
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0;
|
|
|
|
// Memory
|
|
const memUsage = containerStats.memory_stats.usage || 0;
|
|
const memLimit = containerStats.memory_stats.limit || 1;
|
|
|
|
// Network
|
|
let netRx = 0, netTx = 0;
|
|
if (containerStats.networks) {
|
|
for (const net of Object.values(containerStats.networks)) {
|
|
netRx += net.rx_bytes || 0;
|
|
netTx += net.tx_bytes || 0;
|
|
}
|
|
}
|
|
|
|
success(res, {
|
|
stats: {
|
|
name: info.Name.replace(/^\//, ''),
|
|
image: info.Config.Image,
|
|
status: info.State.Status,
|
|
started: info.State.StartedAt,
|
|
cpu: {
|
|
percent: Math.round(cpuPercent * 100) / 100
|
|
},
|
|
memory: {
|
|
used: memUsage,
|
|
limit: memLimit,
|
|
percent: Math.round((memUsage / memLimit) * 100 * 100) / 100
|
|
},
|
|
network: { rx: netRx, tx: netTx }
|
|
}
|
|
});
|
|
}, 'stats-container'));
|
|
|
|
return router;
|
|
};
|