From 257406e32c196e26f31b46904631e443752cc1c1 Mon Sep 17 00:00:00 2001 From: MCHost Date: Thu, 3 Jul 2025 05:51:59 -0400 Subject: [PATCH] first commit --- .gitignore | 3 + README.md | 95 +++++++++ default.env | 4 + package.json | 22 +++ status.html | 497 +++++++++++++++++++++++++++++++++++++++++++++++ system-status.js | 214 ++++++++++++++++++++ 6 files changed, 835 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 default.env create mode 100644 package.json create mode 100644 status.html create mode 100644 system-status.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f5e467 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +.env \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fbbfbe --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# My-MC.Link Status + +A real-time server monitoring application for Docker containers and system metrics, integrated with Netdata and Holesail process tracking. This project provides a web-based dashboard to monitor Docker container performance (CPU, memory, network) and host system metrics (CPU, RAM, network, disk I/O). + +## Features + +- **Docker Monitoring**: Tracks running containers, CPU usage, memory usage, and network traffic for containers prefixed with `/mc_`. +- **System Metrics**: Displays host CPU, RAM, network, and disk I/O metrics via Netdata integration. +- **Holesail Process Tracking**: Monitors the number of active Holesail processes. +- **Real-Time Updates**: Uses WebSocket to push updates every second to the frontend. +- **Interactive Charts**: Visualizes data with Chart.js for Docker network traffic and system metrics. +- **Responsive UI**: Styled with a Minecraft-themed design, including particle effects and gradient text. + +## Prerequisites + +- **Node.js**: Version 18 or higher. +- **Docker**: Installed and running with access to the Docker socket. +- **Netdata**: Installed and accessible via a URL specified in the environment variables. +- **Holesail**: Required for process tracking (optional, depending on usage). + +## Installation + +1. **Clone the Repository**: + ```bash + git clone + cd my-mc-stats-website + ``` + +2. **Install Dependencies**: + ```bash + npm install + ``` + +3. **Set Up Environment Variables**: + Create a `.env` file in the project root and configure the following: + ```env + DOCKER_SOCKET_PATH=/var/run/docker.sock + NETDATA_URL=http://:19999/api/v1 + TOTAL_CORES= + PORT=3000 + ``` + +4. **Run the Application**: + - For production: + ```bash + npm start + ``` + - For development (with hot reloading): + ```bash + npm run dev + ``` + +5. **Access the Dashboard**: + Open your browser and navigate to `http://localhost:3000` (or the port specified in `.env`). + +## Project Structure + +- **`system-status.js`**: The main server-side script that: + - Sets up an Express server and WebSocket connection. + - Fetches Docker container stats using `dockerode`. + - Retrieves Netdata metrics via HTTP requests. + - Tracks Holesail process counts. + - Sends real-time updates to clients via WebSocket. +- **`status.html`**: The frontend dashboard that: + - Displays Docker and system metrics in tables and charts. + - Uses Chart.js for data visualization. + - Connects to the WebSocket server for real-time updates. + - Includes Minecraft-themed styling and particle effects. +- **`package.json`**: Defines project metadata, scripts, and dependencies. + +## Usage + +- **Dashboard Overview**: + - **Docker Environment**: Shows total/running containers, CPU/memory usage, Holesail processes, disk usage, and AI fault counts. + - **Container Tables**: Lists Minecraft containers sorted by CPU and memory usage. + - **Host System Metrics**: Displays CPU, RAM, network, and disk I/O charts. +- **Monitoring**: + - Data updates every second via WebSocket. + - Network and disk metrics are smoothed for better visualization. + - Dynamic unit scaling (B/s, KB/s, MB/s, GB/s) for network and disk charts. + +## Dependencies + +- **axios**: For making HTTP requests to Netdata. +- **dockerode**: For interacting with the Docker API. +- **dotenv**: For loading environment variables. +- **express**: For the web server. +- **ws**: For WebSocket communication. +- **nodemon** (dev): For hot reloading during development. + +## Acknowledgments + +- Powered by [Holesail](https://holesail.io). +- Services donated by [SNXRaven](https://raven-scott.fyi). +- Built with [Chart.js](https://www.chartjs.org) and [Netdata](https://www.netdata.cloud). \ No newline at end of file diff --git a/default.env b/default.env new file mode 100644 index 0000000..3fad218 --- /dev/null +++ b/default.env @@ -0,0 +1,4 @@ +NETDATA_URL=http://x.x.x.x:19999/api/v1 +TOTAL_CORES=50 +DOCKER_SOCKET_PATH=/var/run/docker.sock +PORT=3000 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4da9fd7 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "my-mc-stats-website", + "version": "1.0.0", + "description": "A server monitoring application with Docker and Netdata integration", + "main": "index.js", + "scripts": { + "start": "node system-status.js", + "dev": "nodemon system-status.js" + }, + "dependencies": { + "axios": "^1.7.7", + "dockerode": "^4.0.2", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "nodemon": "^3.1.7" + }, + "author": "", + "license": "ISC" + } \ No newline at end of file diff --git a/status.html b/status.html new file mode 100644 index 0000000..02ff3d2 --- /dev/null +++ b/status.html @@ -0,0 +1,497 @@ + + + + + + My-MC.Link Status + + + + + + + + + +
+
+
+
+
+ +
+
+
+

+ My-MC.Link Status

+

Real-Time System & Docker Monitoring

+
+ Back to Home +
+
+ +
+
+

+ Docker Environment Overview

+
+
+

Running Containers

+

0

+
+
+

Total Containers

+

0

+
+
+

Total CPU Usage

+

0%

+
+
+

Total RAM Usage

+

0 GB

+
+
+

Holesail Processes

+

0

+
+
+

Disk Usage

+

0 GB

+
+
+

Free Disk

+

0 GB

+
+
+

AI Fault Monitor Count

+

0

+
+
+

Docker Network Traffic

+ +

Minecraft Containers (Sorted by CPU)

+
+ + + + + + + + + + +
NameCPU Usage (%)Memory Usage (MB)State
+
+

Minecraft Containers (Sorted by Memory)

+
+ + + + + + + + + + +
NameCPU Usage (%)Memory Usage (MB)State
+
+
+ +
+

+ Host System Metrics

+
+
+

CPU Usage

+ +
+
+

RAM Usage

+ +
+
+

Network Traffic

+ +
+
+

Disk I/O

+ +
+
+
+
+ +
+

© 2025 My-MC.Link. All rights reserved.

+

+ Powered by Holesail with services + donated by SNXRaven +

+
+ + + + \ No newline at end of file diff --git a/system-status.js b/system-status.js new file mode 100644 index 0000000..5042451 --- /dev/null +++ b/system-status.js @@ -0,0 +1,214 @@ +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}`); +}); \ No newline at end of file