214 lines
8.9 KiB
JavaScript
214 lines
8.9 KiB
JavaScript
const express = require('express');
|
|
const http = require('http');
|
|
const WebSocket = require('ws');
|
|
const axios = require('axios');
|
|
const Docker = require('dockerode');
|
|
const { exec } = require('child_process');
|
|
const util = require('util');
|
|
require('dotenv').config();
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const wss = new WebSocket.Server({ server });
|
|
const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET_PATH });
|
|
const NETDATA_URL = process.env.NETDATA_URL;
|
|
const TOTAL_CORES = parseInt(process.env.TOTAL_CORES, 10);
|
|
|
|
// Promisify exec for async/await
|
|
const execPromise = util.promisify(exec);
|
|
|
|
// Store previous stats for rate calculations
|
|
let prevStats = new Map();
|
|
|
|
// Helper to format bytes dynamically
|
|
function formatBytes(bytes) {
|
|
if (bytes >= 1e9) return { value: (bytes / 1e9).toFixed(2), unit: 'GB/s' };
|
|
if (bytes >= 1e6) return { value: (bytes / 1e6).toFixed(2), unit: 'MB/s' };
|
|
if (bytes >= 1e3) return { value: (bytes / 1e3).toFixed(2), unit: 'KB/s' };
|
|
return { value: bytes.toFixed(2), unit: 'B/s' };
|
|
}
|
|
|
|
// Fetch Holesail process count
|
|
async function getHolesailProcessCount() {
|
|
try {
|
|
const { stdout } = await execPromise('ps auxf | grep holesail | wc -l');
|
|
return parseInt(stdout.trim(), 10);
|
|
} catch (error) {
|
|
console.error('Error fetching Holesail process count:', error.message);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Fetch Docker container stats
|
|
async function getDockerStats() {
|
|
try {
|
|
const containers = await docker.listContainers({ all: true });
|
|
const runningContainers = containers.filter(c => c.State === 'running');
|
|
const mcContainers = runningContainers.filter(c => c.Names.some(name => name.startsWith('/mc_')));
|
|
|
|
// Get container stats
|
|
const containerStats = await Promise.all(
|
|
mcContainers.map(async (container) => {
|
|
const containerInfo = docker.getContainer(container.Id);
|
|
const stats = await containerInfo.stats({ stream: false });
|
|
const containerId = container.Id;
|
|
|
|
// CPU usage calculation
|
|
const prev = prevStats.get(containerId) || {
|
|
cpu_usage: stats.cpu_stats.cpu_usage.total_usage,
|
|
system_cpu: stats.cpu_stats.system_cpu_usage,
|
|
time: Date.now()
|
|
};
|
|
|
|
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - prev.cpu_usage;
|
|
const systemDelta = stats.cpu_stats.system_cpu_usage - prev.system_cpu;
|
|
const now = Date.now();
|
|
const timeDiffMs = now - prev.time;
|
|
|
|
// Calculate CPU usage as percentage of total CPU capacity
|
|
let cpuUsage = 0;
|
|
if (systemDelta > 0 && timeDiffMs > 0) {
|
|
cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus / TOTAL_CORES * 100;
|
|
}
|
|
|
|
// Memory usage
|
|
const memoryUsage = stats.memory_stats.usage / 1024 / 1024; // MB
|
|
|
|
// Network stats
|
|
const networkStats = stats.networks?.eth0 || { rx_bytes: 0, tx_bytes: 0 };
|
|
let receivedRate = 0;
|
|
let sentRate = 0;
|
|
const prevNetwork = prev.network || { rx_bytes: 0, tx_bytes: 0 };
|
|
|
|
if (timeDiffMs > 0) {
|
|
const timeDiffSec = timeDiffMs / 1000;
|
|
receivedRate = (networkStats.rx_bytes - prevNetwork.rx_bytes) / timeDiffSec; // bytes/s
|
|
sentRate = (networkStats.tx_bytes - prevNetwork.tx_bytes) / timeDiffSec; // bytes/s
|
|
}
|
|
|
|
// Update previous stats
|
|
prevStats.set(containerId, {
|
|
cpu_usage: stats.cpu_stats.cpu_usage.total_usage,
|
|
system_cpu: stats.cpu_stats.system_cpu_usage,
|
|
network: { rx_bytes: networkStats.rx_bytes, tx_bytes: networkStats.tx_bytes },
|
|
time: now
|
|
});
|
|
|
|
return {
|
|
id: containerId.substring(0, 12),
|
|
name: container.Names[0].replace(/^\//, ''),
|
|
cpu: cpuUsage.toFixed(2),
|
|
memory: memoryUsage.toFixed(2),
|
|
network: {
|
|
received: formatBytes(receivedRate),
|
|
sent: formatBytes(sentRate)
|
|
},
|
|
state: container.State
|
|
};
|
|
})
|
|
);
|
|
|
|
// Sort by CPU and memory
|
|
const sortedByCpu = [...containerStats].sort((a, b) => b.cpu - a.cpu);
|
|
const sortedByMemory = [...containerStats].sort((a, b) => b.memory - a.memory);
|
|
|
|
// Aggregate totals
|
|
const totalCpu = containerStats.reduce((sum, c) => sum + parseFloat(c.cpu), 0).toFixed(2);
|
|
const totalMemory = (containerStats.reduce((sum, c) => sum + parseFloat(c.memory), 0) / 1024).toFixed(2); // Convert MB to GB
|
|
const totalNetwork = containerStats.reduce((sum, c) => ({
|
|
received: sum.received + parseFloat(c.network.received.value) * (c.network.received.unit === 'GB/s' ? 1e9 : c.network.received.unit === 'MB/s' ? 1e6 : c.network.received.unit === 'KB/s' ? 1e3 : 1),
|
|
sent: sum.sent + parseFloat(c.network.sent.value) * (c.network.sent.unit === 'GB/s' ? 1e9 : c.network.sent.unit === 'MB/s' ? 1e6 : c.network.sent.unit === 'KB/s' ? 1e3 : 1)
|
|
}), { received: 0, sent: 0 });
|
|
|
|
// Clean up prevStats for stopped containers
|
|
const currentContainerIds = new Set(mcContainers.map(c => c.Id));
|
|
for (const id of prevStats.keys()) {
|
|
if (!currentContainerIds.has(id)) {
|
|
prevStats.delete(id);
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalContainers: containers.length - 3, // Exclude 3 system containers
|
|
runningContainers: runningContainers.length - 3,
|
|
totalCpu,
|
|
totalMemory,
|
|
totalNetwork: {
|
|
received: formatBytes(totalNetwork.received),
|
|
sent: formatBytes(totalNetwork.sent),
|
|
time: Math.floor(Date.now() / 1000)
|
|
},
|
|
sortedByCpu,
|
|
sortedByMemory
|
|
};
|
|
} catch (error) {
|
|
console.error('Error fetching Docker stats:', error.message);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Fetch Netdata metrics
|
|
async function getNetdataMetrics() {
|
|
try {
|
|
const charts = [
|
|
{ key: 'cpu', url: `${NETDATA_URL}/data?chart=system.cpu&after=-60&points=30`, map: d => ({ time: d[0], user: d[6], system: d[7] }) },
|
|
{ key: 'ram', url: `${NETDATA_URL}/data?chart=system.ram&after=-60&points=30`, map: d => ({ time: d[0], used: d[2], free: d[3] }) },
|
|
{ key: 'net', url: `${NETDATA_URL}/data?chart=system.net&after=-60&points=30`, map: d => ({ time: d[0], received: d[1], sent: d[2] }) },
|
|
{ key: 'disk', url: `${NETDATA_URL}/data?chart=system.io&after=-60&points=30`, map: d => ({ time: d[0], in: d[1], out: d[2] }) },
|
|
{ key: 'disk_space', url: `${NETDATA_URL}/data?chart=disk_space./&format=json&after=-60&points=30`, map: d => ({ time: d[0], avail: d[1], used: d[2], reserved: d[3] }) },
|
|
{ key: 'anomaly', url: `${NETDATA_URL}/data?chart=anomaly_detection.dimensions_on_mchost&format=json&after=-60&points=30`, map: d => ({ time: d[0], anomalous: d[1], normal: d[2] }) }
|
|
];
|
|
|
|
const results = await Promise.all(
|
|
charts.map(async ({ key, url, map }) => {
|
|
try {
|
|
const response = await axios.get(url);
|
|
const data = response.data.data.map(map);
|
|
return { key, data };
|
|
} catch (error) {
|
|
console.warn(`Failed to fetch Netdata chart ${key}:`, error.message);
|
|
return { key, data: [] };
|
|
}
|
|
})
|
|
);
|
|
|
|
const metrics = {};
|
|
results.forEach(({ key, data }) => {
|
|
metrics[key] = data;
|
|
});
|
|
|
|
return metrics;
|
|
} catch (error) {
|
|
console.error('Error fetching Netdata metrics:', error.message);
|
|
return { cpu: [], ram: [], net: [], disk: [], disk_space: [], anomaly: [] };
|
|
}
|
|
}
|
|
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(__dirname + '/status.html');
|
|
});
|
|
|
|
// WebSocket connection
|
|
wss.on('connection', (ws) => {
|
|
console.log('WebSocket client connected');
|
|
|
|
// Send updates every 1 second
|
|
const interval = setInterval(async () => {
|
|
const [dockerStats, netdataMetrics, holesailProcessCount] = await Promise.all([
|
|
getDockerStats(),
|
|
getNetdataMetrics(),
|
|
getHolesailProcessCount()
|
|
]);
|
|
|
|
ws.send(JSON.stringify({ docker: dockerStats, netdata: netdataMetrics, holesailProcessCount }));
|
|
}, 1000);
|
|
|
|
ws.on('close', () => {
|
|
console.log('WebSocket client disconnected');
|
|
clearInterval(interval);
|
|
});
|
|
});
|
|
|
|
server.listen(process.env.PORT, () => {
|
|
console.log(`Server running on http://localhost:${process.env.PORT}`);
|
|
}); |