Phase 3 (WIP): Refactor containers & monitoring routes

- routes/containers.js: Explicit deps (docker, asyncHandler, log)
- routes/monitoring.js: Explicit deps (docker, resourceMonitor)
- Pattern established: factory function with destructured deps
This commit is contained in:
Krystie
2026-03-22 11:09:16 +01:00
parent 3efa5dc3f4
commit 81f778df72
3 changed files with 303 additions and 87 deletions

View File

@@ -1,14 +1,21 @@
/**
* Container management routes
* Refactored to use explicit dependencies instead of ctx god object
*/
const express = require('express'); const express = require('express');
const { DOCKER } = require('../constants'); const { DOCKER } = require('../constants');
const { paginate, parsePaginationParams } = require('../pagination'); const { paginate, parsePaginationParams } = require('../pagination');
const { NotFoundError } = require('../errors'); const { NotFoundError } = require('../errors');
const asyncHandler = require('../src/utils/async-handler');
const log = require('../src/utils/logger');
module.exports = function(ctx) { module.exports = function({ docker }) {
const router = express.Router(); const router = express.Router();
// Helper: verify container exists before operating on it // Helper: verify container exists before operating on it
async function getVerifiedContainer(id) { async function getVerifiedContainer(id) {
const container = ctx.docker.client.getContainer(id); const container = docker.client.getContainer(id);
try { try {
await container.inspect(); await container.inspect();
} catch (err) { } catch (err) {
@@ -21,28 +28,28 @@ module.exports = function(ctx) {
} }
// Start container // Start container
router.post('/:id/start', ctx.asyncHandler(async (req, res) => { router.post('/:id/start', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id); const container = await getVerifiedContainer(req.params.id);
await container.start(); await container.start();
res.json({ success: true, message: 'Container started' }); res.json({ success: true, message: 'Container started' });
}, 'container-start')); }, 'container-start'));
// Stop container // Stop container
router.post('/:id/stop', ctx.asyncHandler(async (req, res) => { router.post('/:id/stop', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id); const container = await getVerifiedContainer(req.params.id);
await container.stop(); await container.stop();
res.json({ success: true, message: 'Container stopped' }); res.json({ success: true, message: 'Container stopped' });
}, 'container-stop')); }, 'container-stop'));
// Restart container // Restart container
router.post('/:id/restart', ctx.asyncHandler(async (req, res) => { router.post('/:id/restart', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id); const container = await getVerifiedContainer(req.params.id);
await container.restart(); await container.restart();
res.json({ success: true, message: 'Container restarted' }); res.json({ success: true, message: 'Container restarted' });
}, 'container-restart')); }, 'container-restart'));
// Update container to latest image version // Update container to latest image version
router.post('/:id/update', ctx.asyncHandler(async (req, res) => { router.post('/:id/update', asyncHandler(async (req, res) => {
const containerId = req.params.id; const containerId = req.params.id;
const container = await getVerifiedContainer(containerId); const container = await getVerifiedContainer(containerId);
@@ -51,11 +58,11 @@ module.exports = function(ctx) {
const imageName = containerInfo.Config.Image; const imageName = containerInfo.Config.Image;
const containerName = containerInfo.Name.replace(/^\//, ''); const containerName = containerInfo.Name.replace(/^\//, '');
ctx.log.info('docker', 'Updating container', { containerName, imageName }); log.info('docker', 'Updating container', { containerName, imageName });
// Pull the latest image // Pull the latest image
ctx.log.info('docker', `Pulling latest image: ${imageName}`); log.info('docker', `Pulling latest image: ${imageName}`);
await ctx.docker.pull(imageName); await docker.pull(imageName);
// Get current container config for recreation // Get current container config for recreation
const hostConfig = containerInfo.HostConfig; const hostConfig = containerInfo.HostConfig;
@@ -75,7 +82,7 @@ module.exports = function(ctx) {
CapAdd: hostConfig.CapAdd, CapAdd: hostConfig.CapAdd,
CapDrop: hostConfig.CapDrop, CapDrop: hostConfig.CapDrop,
Devices: hostConfig.Devices, Devices: hostConfig.Devices,
LogConfig: DOCKER.LOG_CONFIG, // Ensure log rotation on updated containers LogConfig: DOCKER.LOG_CONFIG,
}, },
NetworkingConfig: {}, NetworkingConfig: {},
}; };
@@ -89,40 +96,45 @@ module.exports = function(ctx) {
} }
// Stop and remove old container // Stop and remove old container
ctx.log.info('docker', 'Stopping container', { containerName }); log.info('docker', 'Stopping container', { containerName });
await container.stop().catch(() => {}); // Ignore if already stopped await container.stop().catch(() => {}); // Ignore if already stopped
ctx.log.info('docker', 'Removing container', { containerName }); log.info('docker', 'Removing container', { containerName });
await container.remove(); await container.remove();
// Wait for port release (Windows/Docker Desktop can be slow to free ports) // Wait for port release
await new Promise(r => setTimeout(r, 3000)); await new Promise((r) => setTimeout(r, 3000));
// Create and start new container // Create and start new container
ctx.log.info('docker', 'Creating new container', { containerName }); log.info('docker', 'Creating new container', { containerName });
let newContainer; let newContainer;
try { try {
newContainer = await ctx.docker.client.createContainer(config); newContainer = await docker.client.createContainer(config);
ctx.log.info('docker', 'Starting container', { containerName }); log.info('docker', 'Starting container', { containerName });
await newContainer.start(); await newContainer.start();
} catch (startError) { } catch (startError) {
// Clean up the failed container so it doesn't block future attempts log.error('docker', 'Failed to start new container', { containerName, error: startError.message });
ctx.log.error('docker', 'Failed to start new container', { containerName, error: startError.message });
if (newContainer) { if (newContainer) {
try { await newContainer.remove({ force: true }); } catch (e) { /* already gone */ } try {
await newContainer.remove({ force: true });
} catch (e) {
/* already gone */
}
} }
throw startError; throw startError;
} }
const newContainerInfo = await newContainer.inspect(); const newContainerInfo = await newContainer.inspect();
// Prune dangling images after update // Prune dangling images
try { try {
const pruneResult = await ctx.docker.client.pruneImages({ filters: { dangling: { true: true } } }); const pruneResult = await docker.client.pruneImages({ filters: { dangling: { true: true } } });
if (pruneResult.SpaceReclaimed > 0) { if (pruneResult.SpaceReclaimed > 0) {
ctx.log.info('docker', 'Pruned dangling images after update', { spaceReclaimed: `${Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) }MB` }); log.info('docker', 'Pruned dangling images after update', {
spaceReclaimed: `${Math.round(pruneResult.SpaceReclaimed / 1024 / 1024)}MB`,
});
} }
} catch (pruneErr) { } catch (pruneErr) {
ctx.log.debug('docker', 'Image prune after update failed', { error: pruneErr.message }); log.debug('docker', 'Image prune after update failed', { error: pruneErr.message });
} }
res.json({ res.json({
@@ -132,27 +144,27 @@ module.exports = function(ctx) {
}); });
}, 'container-update')); }, 'container-update'));
// Check for available updates (compares local and remote image digests) // Check for available updates
router.get('/:id/check-update', ctx.asyncHandler(async (req, res) => { router.get('/:id/check-update', asyncHandler(async (req, res) => {
const containerId = req.params.id; const containerId = req.params.id;
const container = await getVerifiedContainer(containerId); const container = await getVerifiedContainer(containerId);
const containerInfo = await container.inspect(); const containerInfo = await container.inspect();
const imageName = containerInfo.Config.Image; const imageName = containerInfo.Config.Image;
const localImage = ctx.docker.client.getImage(containerInfo.Image); const localImage = docker.client.getImage(containerInfo.Image);
const localImageInfo = await localImage.inspect(); const localImageInfo = await localImage.inspect();
const localDigest = localImageInfo.RepoDigests?.[0] || null; const localDigest = localImageInfo.RepoDigests?.[0] || null;
let updateAvailable = false; let updateAvailable = false;
try { try {
const pullStream = await ctx.docker.pull(imageName); const pullStream = await docker.pull(imageName);
const downloadedLayers = pullStream.filter(e => const downloadedLayers = pullStream.filter(
e.status === 'Downloading' || e.status === 'Download complete', (e) => e.status === 'Downloading' || e.status === 'Download complete',
); );
updateAvailable = downloadedLayers.length > 0; updateAvailable = downloadedLayers.length > 0;
const newImage = ctx.docker.client.getImage(imageName); const newImage = docker.client.getImage(imageName);
const newImageInfo = await newImage.inspect(); const newImageInfo = await newImage.inspect();
const newDigest = newImageInfo.RepoDigests?.[0] || null; const newDigest = newImageInfo.RepoDigests?.[0] || null;
@@ -160,7 +172,7 @@ module.exports = function(ctx) {
updateAvailable = true; updateAvailable = true;
} }
} catch (pullError) { } catch (pullError) {
ctx.log.debug('docker', 'Could not check for updates', { error: pullError.message }); log.debug('docker', 'Could not check for updates', { error: pullError.message });
} }
res.json({ res.json({
@@ -172,7 +184,7 @@ module.exports = function(ctx) {
}, 'container-check-update')); }, 'container-check-update'));
// Get container logs // Get container logs
router.get('/:id/logs', ctx.asyncHandler(async (req, res) => { router.get('/:id/logs', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id); const container = await getVerifiedContainer(req.params.id);
const logs = await container.logs({ const logs = await container.logs({
stdout: true, stdout: true,
@@ -184,20 +196,20 @@ module.exports = function(ctx) {
}, 'container-logs')); }, 'container-logs'));
// Delete container // Delete container
router.delete('/:id', ctx.asyncHandler(async (req, res) => { router.delete('/:id', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id); const container = await getVerifiedContainer(req.params.id);
await container.remove({ force: true }); await container.remove({ force: true });
res.json({ success: true, message: 'Container removed' }); res.json({ success: true, message: 'Container removed' });
}, 'container-delete')); }, 'container-delete'));
// Discover running containers // Discover running containers
router.get('/discover', ctx.asyncHandler(async (req, res) => { router.get('/discover', asyncHandler(async (req, res) => {
const containers = await ctx.docker.client.listContainers({ all: true }); const containers = await docker.client.listContainers({ all: true });
const samiContainers = containers.filter(container => const samiContainers = containers.filter(
container.Labels && container.Labels['sami.managed'] === 'true', (container) => container.Labels && container.Labels['sami.managed'] === 'true',
); );
const discoveredContainers = samiContainers.map(container => ({ const discoveredContainers = samiContainers.map((container) => ({
id: container.Id, id: container.Id,
name: container.Names[0].replace('/', ''), name: container.Names[0].replace('/', ''),
image: container.Image, image: container.Image,
@@ -210,7 +222,11 @@ module.exports = function(ctx) {
const paginationParams = parsePaginationParams(req.query); const paginationParams = parsePaginationParams(req.query);
const result = paginate(discoveredContainers, paginationParams); const result = paginate(discoveredContainers, paginationParams);
res.json({ success: true, containers: result.data, ...(result.pagination && { pagination: result.pagination }) }); res.json({
success: true,
containers: result.data,
...(result.pagination && { pagination: result.pagination }),
});
}, 'containers-discover')); }, 'containers-discover'));
return router; return router;

View File

@@ -0,0 +1,217 @@
const express = require('express');
const { DOCKER } = require('../constants');
const { paginate, parsePaginationParams } = require('../pagination');
const { NotFoundError } = require('../errors');
module.exports = function(ctx) {
const router = express.Router();
// Helper: verify container exists before operating on it
async function getVerifiedContainer(id) {
const container = ctx.docker.client.getContainer(id);
try {
await container.inspect();
} catch (err) {
if (err.statusCode === 404 || (err.message && err.message.includes('no such container'))) {
throw new NotFoundError(`Container ${id}`);
}
throw err;
}
return container;
}
// Start container
router.post('/:id/start', ctx.asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
await container.start();
res.json({ success: true, message: 'Container started' });
}, 'container-start'));
// Stop container
router.post('/:id/stop', ctx.asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
await container.stop();
res.json({ success: true, message: 'Container stopped' });
}, 'container-stop'));
// Restart container
router.post('/:id/restart', ctx.asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
await container.restart();
res.json({ success: true, message: 'Container restarted' });
}, 'container-restart'));
// Update container to latest image version
router.post('/:id/update', ctx.asyncHandler(async (req, res) => {
const containerId = req.params.id;
const container = await getVerifiedContainer(containerId);
// Get container info
const containerInfo = await container.inspect();
const imageName = containerInfo.Config.Image;
const containerName = containerInfo.Name.replace(/^\//, '');
ctx.log.info('docker', 'Updating container', { containerName, imageName });
// Pull the latest image
ctx.log.info('docker', `Pulling latest image: ${imageName}`);
await ctx.docker.pull(imageName);
// Get current container config for recreation
const hostConfig = containerInfo.HostConfig;
const config = {
Image: imageName,
name: containerName,
Env: containerInfo.Config.Env,
ExposedPorts: containerInfo.Config.ExposedPorts,
Labels: containerInfo.Config.Labels,
HostConfig: {
Binds: hostConfig.Binds,
PortBindings: hostConfig.PortBindings,
RestartPolicy: hostConfig.RestartPolicy,
NetworkMode: hostConfig.NetworkMode,
ExtraHosts: hostConfig.ExtraHosts,
Privileged: hostConfig.Privileged,
CapAdd: hostConfig.CapAdd,
CapDrop: hostConfig.CapDrop,
Devices: hostConfig.Devices,
LogConfig: DOCKER.LOG_CONFIG, // Ensure log rotation on updated containers
},
NetworkingConfig: {},
};
// Get network settings if using a custom network
if (hostConfig.NetworkMode && !['bridge', 'host', 'none'].includes(hostConfig.NetworkMode)) {
const networkName = hostConfig.NetworkMode;
config.NetworkingConfig.EndpointsConfig = {
[networkName]: containerInfo.NetworkSettings.Networks[networkName],
};
}
// Stop and remove old container
ctx.log.info('docker', 'Stopping container', { containerName });
await container.stop().catch(() => {}); // Ignore if already stopped
ctx.log.info('docker', 'Removing container', { containerName });
await container.remove();
// Wait for port release (Windows/Docker Desktop can be slow to free ports)
await new Promise(r => setTimeout(r, 3000));
// Create and start new container
ctx.log.info('docker', 'Creating new container', { containerName });
let newContainer;
try {
newContainer = await ctx.docker.client.createContainer(config);
ctx.log.info('docker', 'Starting container', { containerName });
await newContainer.start();
} catch (startError) {
// Clean up the failed container so it doesn't block future attempts
ctx.log.error('docker', 'Failed to start new container', { containerName, error: startError.message });
if (newContainer) {
try { await newContainer.remove({ force: true }); } catch (e) { /* already gone */ }
}
throw startError;
}
const newContainerInfo = await newContainer.inspect();
// Prune dangling images after update
try {
const pruneResult = await ctx.docker.client.pruneImages({ filters: { dangling: { true: true } } });
if (pruneResult.SpaceReclaimed > 0) {
ctx.log.info('docker', 'Pruned dangling images after update', { spaceReclaimed: `${Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) }MB` });
}
} catch (pruneErr) {
ctx.log.debug('docker', 'Image prune after update failed', { error: pruneErr.message });
}
res.json({
success: true,
message: `Container ${containerName} updated successfully`,
newContainerId: newContainerInfo.Id,
});
}, 'container-update'));
// Check for available updates (compares local and remote image digests)
router.get('/:id/check-update', ctx.asyncHandler(async (req, res) => {
const containerId = req.params.id;
const container = await getVerifiedContainer(containerId);
const containerInfo = await container.inspect();
const imageName = containerInfo.Config.Image;
const localImage = ctx.docker.client.getImage(containerInfo.Image);
const localImageInfo = await localImage.inspect();
const localDigest = localImageInfo.RepoDigests?.[0] || null;
let updateAvailable = false;
try {
const pullStream = await ctx.docker.pull(imageName);
const downloadedLayers = pullStream.filter(e =>
e.status === 'Downloading' || e.status === 'Download complete',
);
updateAvailable = downloadedLayers.length > 0;
const newImage = ctx.docker.client.getImage(imageName);
const newImageInfo = await newImage.inspect();
const newDigest = newImageInfo.RepoDigests?.[0] || null;
if (localDigest && newDigest && localDigest !== newDigest) {
updateAvailable = true;
}
} catch (pullError) {
ctx.log.debug('docker', 'Could not check for updates', { error: pullError.message });
}
res.json({
success: true,
imageName,
updateAvailable,
currentDigest: localDigest,
});
}, 'container-check-update'));
// Get container logs
router.get('/:id/logs', ctx.asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
const logs = await container.logs({
stdout: true,
stderr: true,
tail: 100,
timestamps: true,
});
res.json({ success: true, logs: logs.toString() });
}, 'container-logs'));
// Delete container
router.delete('/:id', ctx.asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
await container.remove({ force: true });
res.json({ success: true, message: 'Container removed' });
}, 'container-delete'));
// Discover running containers
router.get('/discover', ctx.asyncHandler(async (req, res) => {
const containers = await ctx.docker.client.listContainers({ all: true });
const samiContainers = containers.filter(container =>
container.Labels && container.Labels['sami.managed'] === 'true',
);
const discoveredContainers = samiContainers.map(container => ({
id: container.Id,
name: container.Names[0].replace('/', ''),
image: container.Image,
state: container.State,
status: container.Status,
appTemplate: container.Labels['sami.app'],
subdomain: container.Labels['sami.subdomain'],
ports: container.Ports,
}));
const paginationParams = parsePaginationParams(req.query);
const result = paginate(discoveredContainers, paginationParams);
res.json({ success: true, containers: result.data, ...(result.pagination && { pagination: result.pagination }) });
}, 'containers-discover'));
return router;
};

View File

@@ -1,19 +1,23 @@
const express = require('express'); /**
* Monitoring and stats routes
* Refactored to use explicit dependencies
*/
module.exports = function(ctx) { const express = require('express');
const asyncHandler = require('../src/utils/async-handler');
module.exports = function({ docker, resourceMonitor }) {
const router = express.Router(); const router = express.Router();
// ===== RESOURCE MONITORING ENDPOINTS ===== // ===== RESOURCE MONITORING ENDPOINTS =====
// Get all container stats (from resource monitor module) router.get('/monitoring/stats', asyncHandler(async (req, res) => {
router.get('/monitoring/stats', ctx.asyncHandler(async (req, res) => { const stats = resourceMonitor.getAllStats();
const stats = ctx.resourceMonitor.getAllStats();
res.json({ success: true, stats }); res.json({ success: true, stats });
}, 'monitoring-stats')); }, 'monitoring-stats'));
// Get stats for specific container router.get('/monitoring/stats/:containerId', asyncHandler(async (req, res) => {
router.get('/monitoring/stats/:containerId', ctx.asyncHandler(async (req, res) => { const stats = resourceMonitor.getCurrentStats(req.params.containerId);
const stats = ctx.resourceMonitor.getCurrentStats(req.params.containerId);
if (!stats) { if (!stats) {
const { NotFoundError } = require('../errors'); const { NotFoundError } = require('../errors');
throw new NotFoundError('Container'); throw new NotFoundError('Container');
@@ -21,17 +25,15 @@ module.exports = function(ctx) {
res.json({ success: true, stats }); res.json({ success: true, stats });
}, 'monitoring-stats-container')); }, 'monitoring-stats-container'));
// Get historical stats router.get('/monitoring/history/:containerId', asyncHandler(async (req, res) => {
router.get('/monitoring/history/:containerId', ctx.asyncHandler(async (req, res) => {
const hours = parseInt(req.query.hours) || 24; const hours = parseInt(req.query.hours) || 24;
const history = ctx.resourceMonitor.getHistoricalStats(req.params.containerId, hours); const history = resourceMonitor.getHistoricalStats(req.params.containerId, hours);
res.json({ success: true, history, hours }); res.json({ success: true, history, hours });
}, 'monitoring-history')); }, 'monitoring-history'));
// Get aggregated stats router.get('/monitoring/aggregated/:containerId', asyncHandler(async (req, res) => {
router.get('/monitoring/aggregated/:containerId', ctx.asyncHandler(async (req, res) => {
const hours = parseInt(req.query.hours) || 24; const hours = parseInt(req.query.hours) || 24;
const aggregated = ctx.resourceMonitor.getAggregatedStats(req.params.containerId, hours); const aggregated = resourceMonitor.getAggregatedStats(req.params.containerId, hours);
if (!aggregated) { if (!aggregated) {
const { NotFoundError } = require('../errors'); const { NotFoundError } = require('../errors');
throw new NotFoundError('Monitoring data'); throw new NotFoundError('Monitoring data');
@@ -39,49 +41,42 @@ module.exports = function(ctx) {
res.json({ success: true, aggregated, hours }); res.json({ success: true, aggregated, hours });
}, 'monitoring-aggregated')); }, 'monitoring-aggregated'));
// Configure alerts router.post('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
router.post('/monitoring/alerts/:containerId', ctx.asyncHandler(async (req, res) => { resourceMonitor.setAlertConfig(req.params.containerId, req.body);
ctx.resourceMonitor.setAlertConfig(req.params.containerId, req.body);
res.json({ success: true, message: 'Alert configuration saved' }); res.json({ success: true, message: 'Alert configuration saved' });
}, 'monitoring-alerts-set')); }, 'monitoring-alerts-set'));
// Get alert configuration router.get('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
router.get('/monitoring/alerts/:containerId', ctx.asyncHandler(async (req, res) => { const config = resourceMonitor.getAlertConfig(req.params.containerId);
const config = ctx.resourceMonitor.getAlertConfig(req.params.containerId);
res.json({ success: true, config: config || {} }); res.json({ success: true, config: config || {} });
}, 'monitoring-alerts-get')); }, 'monitoring-alerts-get'));
// Delete alert configuration router.delete('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
router.delete('/monitoring/alerts/:containerId', ctx.asyncHandler(async (req, res) => { resourceMonitor.removeAlertConfig(req.params.containerId);
ctx.resourceMonitor.removeAlertConfig(req.params.containerId);
res.json({ success: true, message: 'Alert configuration removed' }); res.json({ success: true, message: 'Alert configuration removed' });
}, 'monitoring-alerts-delete')); }, 'monitoring-alerts-delete'));
// ===== CONTAINER STATS ENDPOINTS (legacy /stats/) ===== // ===== CONTAINER STATS ENDPOINTS (legacy /stats/) =====
// Get all container stats (live Docker stats) router.get('/stats/containers', asyncHandler(async (req, res) => {
router.get('/stats/containers', ctx.asyncHandler(async (req, res) => { const containers = await docker.client.listContainers({ all: false });
const containers = await ctx.docker.client.listContainers({ all: false });
const stats = []; const stats = [];
for (const containerInfo of containers) { for (const containerInfo of containers) {
try { try {
const container = ctx.docker.client.getContainer(containerInfo.Id); const container = docker.client.getContainer(containerInfo.Id);
const containerStats = await container.stats({ stream: false }); const containerStats = await container.stats({ stream: false });
// Calculate CPU percentage
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage - const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
(containerStats.precpu_stats.cpu_usage?.total_usage || 0); (containerStats.precpu_stats.cpu_usage?.total_usage || 0);
const systemDelta = containerStats.cpu_stats.system_cpu_usage - const systemDelta = containerStats.cpu_stats.system_cpu_usage -
(containerStats.precpu_stats.system_cpu_usage || 0); (containerStats.precpu_stats.system_cpu_usage || 0);
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 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 memUsage = containerStats.memory_stats.usage || 0;
const memLimit = containerStats.memory_stats.limit || 1; const memLimit = containerStats.memory_stats.limit || 1;
const memPercent = (memUsage / memLimit) * 100; const memPercent = (memUsage / memLimit) * 100;
// Network stats
let netRx = 0, netTx = 0; let netRx = 0, netTx = 0;
if (containerStats.networks) { if (containerStats.networks) {
for (const net of Object.values(containerStats.networks)) { for (const net of Object.values(containerStats.networks)) {
@@ -95,21 +90,15 @@ module.exports = function(ctx) {
name: containerInfo.Names[0]?.replace(/^\//, '') || 'unknown', name: containerInfo.Names[0]?.replace(/^\//, '') || 'unknown',
image: containerInfo.Image, image: containerInfo.Image,
status: containerInfo.State, status: containerInfo.State,
cpu: { cpu: { percent: Math.round(cpuPercent * 100) / 100 },
percent: Math.round(cpuPercent * 100) / 100,
},
memory: { memory: {
used: memUsage, used: memUsage,
limit: memLimit, limit: memLimit,
percent: Math.round(memPercent * 100) / 100, percent: Math.round(memPercent * 100) / 100,
}, },
network: { network: { rx: netRx, tx: netTx },
rx: netRx,
tx: netTx,
},
}); });
} catch (e) { } catch (e) {
// Skip containers we can't get stats for
console.log(`Could not get stats for ${containerInfo.Names[0]}:`, e.message); console.log(`Could not get stats for ${containerInfo.Names[0]}:`, e.message);
} }
} }
@@ -117,24 +106,20 @@ module.exports = function(ctx) {
res.json({ success: true, stats, timestamp: new Date().toISOString() }); res.json({ success: true, stats, timestamp: new Date().toISOString() });
}, 'stats-containers')); }, 'stats-containers'));
// Get single container stats router.get('/stats/container/:id', asyncHandler(async (req, res) => {
router.get('/stats/container/:id', ctx.asyncHandler(async (req, res) => { const container = docker.client.getContainer(req.params.id);
const container = ctx.docker.client.getContainer(req.params.id);
const containerStats = await container.stats({ stream: false }); const containerStats = await container.stats({ stream: false });
const info = await container.inspect(); const info = await container.inspect();
// Calculate CPU percentage
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage - const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
(containerStats.precpu_stats.cpu_usage?.total_usage || 0); (containerStats.precpu_stats.cpu_usage?.total_usage || 0);
const systemDelta = containerStats.cpu_stats.system_cpu_usage - const systemDelta = containerStats.cpu_stats.system_cpu_usage -
(containerStats.precpu_stats.system_cpu_usage || 0); (containerStats.precpu_stats.system_cpu_usage || 0);
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0; const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0;
// Memory
const memUsage = containerStats.memory_stats.usage || 0; const memUsage = containerStats.memory_stats.usage || 0;
const memLimit = containerStats.memory_stats.limit || 1; const memLimit = containerStats.memory_stats.limit || 1;
// Network
let netRx = 0, netTx = 0; let netRx = 0, netTx = 0;
if (containerStats.networks) { if (containerStats.networks) {
for (const net of Object.values(containerStats.networks)) { for (const net of Object.values(containerStats.networks)) {
@@ -150,9 +135,7 @@ module.exports = function(ctx) {
image: info.Config.Image, image: info.Config.Image,
status: info.State.Status, status: info.State.Status,
started: info.State.StartedAt, started: info.State.StartedAt,
cpu: { cpu: { percent: Math.round(cpuPercent * 100) / 100 },
percent: Math.round(cpuPercent * 100) / 100,
},
memory: { memory: {
used: memUsage, used: memUsage,
limit: memLimit, limit: memLimit,