first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
.env
|
95
README.md
Normal file
95
README.md
Normal file
@ -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 <repository-url>
|
||||||
|
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://<your-netdata-host>:19999/api/v1
|
||||||
|
TOTAL_CORES=<number-of-cpu-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).
|
4
default.env
Normal file
4
default.env
Normal file
@ -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
|
22
package.json
Normal file
22
package.json
Normal file
@ -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"
|
||||||
|
}
|
497
status.html
Normal file
497
status.html
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>My-MC.Link Status</title>
|
||||||
|
<link rel="icon" href="https://minecraft.wiki/images/Favicon.png" type="image/png">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700;900&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://my-mc.link/css/style_min.css?p=4" rel="stylesheet">
|
||||||
|
<link href="https://my-mc.link/favicon.ico" rel="icon" type="image/x-icon">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
||||||
|
<style>
|
||||||
|
table th, table td {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
.container-table {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.container-table table {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Particle Effects -->
|
||||||
|
<div class="particle" style="left: 10%; top: 20%; animation-delay: 0s;"></div>
|
||||||
|
<div class="particle large" style="left: 30%; top: 50%; animation-delay: 2s;"></div>
|
||||||
|
<div class="particle" style="left: 50%; top: 30%; animation-delay: 4s;"></div>
|
||||||
|
<div class="particle large" style="left: 70%; top: 60%; animation-delay: 6s;"></div>
|
||||||
|
<div class="particle" style="left: 20%; top: 80%; animation-delay: 8s;"></div>
|
||||||
|
|
||||||
|
<header class="header-bg py-16 text-center relative z-10">
|
||||||
|
<div class="header-content flex items-center justify-between max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-5xl minecraft-font bg-clip-text text-transparent bg-gradient-to-r from-teal-400 to-blue-500">
|
||||||
|
My-MC.Link Status</h1>
|
||||||
|
<p class="text-lg mt-4 opacity-90 tracking-wide font-medium">Real-Time System & Docker Monitoring</p>
|
||||||
|
</div>
|
||||||
|
<a href="https://my-mc.link" class="btn-minecraft text-base self-center">Back to Home</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-16 relative z-10">
|
||||||
|
<section class="section-bg p-8 sm:p-10 mb-12">
|
||||||
|
<h2 class="text-3xl minecraft-font mb-8 text-center bg-clip-text text-transparent bg-gradient-to-r from-teal-400 to-blue-500">
|
||||||
|
Docker Environment Overview</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Running Containers</h3>
|
||||||
|
<p class="text-lg opacity-90" id="running-containers">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Total Containers</h3>
|
||||||
|
<p class="text-lg opacity-90" id="total-containers">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Total CPU Usage</h3>
|
||||||
|
<p class="text-lg opacity-90" id="total-cpu">0%</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Total RAM Usage</h3>
|
||||||
|
<p class="text-lg opacity-90" id="total-ram">0 GB</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Holesail Processes</h3>
|
||||||
|
<p class="text-lg opacity-90" id="holesail-processes">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Disk Usage</h3>
|
||||||
|
<p class="text-lg opacity-90" id="disk-usage">0 GB</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">Free Disk</h3>
|
||||||
|
<p class="text-lg opacity-90" id="free-disk">0 GB</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card tilt-card">
|
||||||
|
<h3 class="text-xl minecraft-font mb-3 text-teal-400">AI Fault Monitor Count</h3>
|
||||||
|
<p class="text-lg opacity-90" id="ai-fault-count">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Docker Network Traffic</h3>
|
||||||
|
<canvas id="docker-net-chart" class="mb-8"></canvas>
|
||||||
|
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Minecraft Containers (Sorted by CPU)</h3>
|
||||||
|
<div class="container-table">
|
||||||
|
<table class="w-full text-sm text-gray-200 mb-8">
|
||||||
|
<thead class="text-xs uppercase bg-gray-800">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3">Name</th>
|
||||||
|
<th class="px-6 py-3">CPU Usage (%)</th>
|
||||||
|
<th class="px-6 py-3">Memory Usage (MB)</th>
|
||||||
|
<th class="px-6 py-3">State</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="cpu-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Minecraft Containers (Sorted by Memory)</h3>
|
||||||
|
<div class="container-table">
|
||||||
|
<table class="w-full text-sm text-gray-200">
|
||||||
|
<thead class="text-xs uppercase bg-gray-800">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3">Name</th>
|
||||||
|
<th class="px-6 py-3">CPU Usage (%)</th>
|
||||||
|
<th class="px-6 py-3">Memory Usage (MB)</th>
|
||||||
|
<th class="px-6 py-3">State</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="memory-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-bg p-8 sm:p-10 mb-12">
|
||||||
|
<h2 class="text-3xl minecraft-font mb-8 text-center bg-gradient-to-r from-teal-400 to-blue-500 bg-clip-text text-transparent">
|
||||||
|
Host System Metrics</h2>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">CPU Usage</h3>
|
||||||
|
<canvas id="cpu-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">RAM Usage</h3>
|
||||||
|
<canvas id="ram-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Network Traffic</h3>
|
||||||
|
<canvas id="net-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl minecraft-font mb-6 text-teal-400 text-center">Disk I/O</h3>
|
||||||
|
<canvas id="disk-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-gray-900/20 backdrop-filter backdrop-blur-xl py-8 text-center relative z-10">
|
||||||
|
<p class="text-sm opacity-90">© 2025 My-MC.Link. All rights reserved.</p>
|
||||||
|
<p class="text-sm opacity-90 mt-3">
|
||||||
|
Powered by <a href="https://holesail.io" class="underline" target="_blank">Holesail</a> with services
|
||||||
|
donated by <a href="https://raven-scott.fyi" class="underline">SNXRaven</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ws = new WebSocket('wss://' + window.location.host);
|
||||||
|
const charts = {};
|
||||||
|
let dockerNetHistory = [];
|
||||||
|
let netHistory = [];
|
||||||
|
let diskHistory = [];
|
||||||
|
const MAX_POINTS = 10;
|
||||||
|
const SMOOTHING_WINDOW = 3;
|
||||||
|
|
||||||
|
// Helper to smooth data
|
||||||
|
function smoothData(data, windowSize) {
|
||||||
|
if (data.length < windowSize) return data;
|
||||||
|
return data.map((_, i) => {
|
||||||
|
const start = Math.max(0, i - windowSize + 1);
|
||||||
|
const slice = data.slice(start, i + 1);
|
||||||
|
return slice.reduce((sum, val) => sum + parseFloat(val), 0) / slice.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to format bytes dynamically (mirrors system-status.js)
|
||||||
|
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' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Chart.js charts
|
||||||
|
function initCharts() {
|
||||||
|
if (typeof Chart === 'undefined') {
|
||||||
|
console.error('Chart.js failed to load.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvases = {
|
||||||
|
dockerNet: document.getElementById('docker-net-chart'),
|
||||||
|
cpu: document.getElementById('cpu-chart'),
|
||||||
|
ram: document.getElementById('ram-chart'),
|
||||||
|
net: document.getElementById('net-chart'),
|
||||||
|
disk: document.getElementById('disk-chart')
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, canvas] of Object.entries(canvases)) {
|
||||||
|
if (!canvas || !canvas.getContext) {
|
||||||
|
console.error(`Canvas element for ${key} not found or invalid.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonOptions = {
|
||||||
|
responsive: true,
|
||||||
|
animation: { duration: 500, easing: 'linear' },
|
||||||
|
scales: {
|
||||||
|
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' } },
|
||||||
|
x: { grid: { display: false } }
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: { mode: 'index', intersect: false },
|
||||||
|
legend: { labels: { color: '#e5e7eb' } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
charts.dockerNet = new Chart(canvases.dockerNet.getContext('2d'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'In', data: [], backgroundColor: '#38bdf8', stack: 'Stack 0' },
|
||||||
|
{ label: 'Out', data: [], backgroundColor: '#fb7185', stack: 'Stack 0' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
stacked: true,
|
||||||
|
title: { display: true, text: 'Speed', color: '#e5e7eb' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
...commonOptions.scales.x,
|
||||||
|
stacked: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
charts.cpu = new Chart(canvases.cpu.getContext('2d'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'User (%)', data: [], borderColor: '#38bdf8', fill: false, tension: 0.3 },
|
||||||
|
{ label: 'System (%)', data: [], borderColor: '#fb7185', fill: false, tension: 0.3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
max: 100,
|
||||||
|
title: { display: true, text: 'Usage (%)', color: '#e5e7eb' }
|
||||||
|
},
|
||||||
|
x: commonOptions.scales.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
charts.ram = new Chart(canvases.ram.getContext('2d'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Used (MB)', data: [], borderColor: '#38bdf8', fill: false, tension: 0.3 },
|
||||||
|
{ label: 'Free (MB)', data: [], borderColor: '#fb7185', fill: false, tension: 0.3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
title: { display: true, text: 'Memory (MB)', color: '#e5e7eb' }
|
||||||
|
},
|
||||||
|
x: commonOptions.scales.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
charts.net = new Chart(canvases.net.getContext('2d'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Received', data: [], backgroundColor: '#38bdf8', stack: 'Stack 0' },
|
||||||
|
{ label: 'Sent', data: [], backgroundColor: '#fb7185', stack: 'Stack 0' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
stacked: true,
|
||||||
|
title: { display: true, text: 'Speed', color: '#e5e7eb' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
...commonOptions.scales.x,
|
||||||
|
stacked: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
charts.disk = new Chart(canvases.disk.getContext('2d'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Read', data: [], borderColor: '#38bdf8', fill: false, tension: 0.3 },
|
||||||
|
{ label: 'Write', data: [], borderColor: '#fb7185', fill: false, tension: 0.3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
title: { display: true, text: 'Speed', color: '#e5e7eb' }
|
||||||
|
},
|
||||||
|
x: commonOptions.scales.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Charts initialized successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI with data
|
||||||
|
function updateUI(data) {
|
||||||
|
if (!data.docker) {
|
||||||
|
console.warn('Missing docker data.');
|
||||||
|
data.docker = {};
|
||||||
|
}
|
||||||
|
if (!data.netdata) {
|
||||||
|
console.warn('Missing netdata data.');
|
||||||
|
data.netdata = { cpu: [], ram: [], net: [], disk: [], disk_space: [], anomaly: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Docker stats
|
||||||
|
document.getElementById('total-containers').textContent = data.docker.totalContainers || 0;
|
||||||
|
document.getElementById('running-containers').textContent = data.docker.runningContainers || 0;
|
||||||
|
document.getElementById('total-cpu').textContent = `${data.docker.totalCpu || 0}%`;
|
||||||
|
document.getElementById('total-ram').textContent = `${data.docker.totalMemory || 0} GB`;
|
||||||
|
document.getElementById('holesail-processes').textContent = data.holesailProcessCount || 0;
|
||||||
|
|
||||||
|
// Update Disk Usage and Free Disk
|
||||||
|
const diskSpaceData = data.netdata.disk_space || [];
|
||||||
|
const latestDiskSpace = diskSpaceData.length > 0 ? diskSpaceData[diskSpaceData.length - 1] : null;
|
||||||
|
document.getElementById('disk-usage').textContent = latestDiskSpace ? `${latestDiskSpace.used.toFixed(2)} GB` : '0 GB';
|
||||||
|
document.getElementById('free-disk').textContent = latestDiskSpace ? `${latestDiskSpace.avail.toFixed(2)} GB` : '0 GB';
|
||||||
|
|
||||||
|
// Update AI Fault Monitor Count
|
||||||
|
const anomalyData = data.netdata.anomaly || [];
|
||||||
|
const latestAnomaly = anomalyData.length > 0 ? anomalyData[anomalyData.length - 1] : null;
|
||||||
|
document.getElementById('ai-fault-count').textContent = latestAnomaly ? `${latestAnomaly.anomalous}` : '0';
|
||||||
|
|
||||||
|
// Update container tables
|
||||||
|
const cpuTable = document.getElementById('cpu-table');
|
||||||
|
const memoryTable = document.getElementById('memory-table');
|
||||||
|
cpuTable.innerHTML = data.docker.sortedByCpu?.map(c => `
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4">${c.name}</td>
|
||||||
|
<td class="px-6 py-4">${c.cpu}%</td>
|
||||||
|
<td class="px-6 py-4">${c.memory} MB</td>
|
||||||
|
<td class="px-6 py-4">${c.state}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('') || '';
|
||||||
|
memoryTable.innerHTML = data.docker.sortedByMemory?.map(c => `
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4">${c.name}</td>
|
||||||
|
<td class="px-6 py-4">${c.cpu}%</td>
|
||||||
|
<td class="px-6 py-4">${c.memory} MB</td>
|
||||||
|
<td class="px-6 py-4">${c.state}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('') || '';
|
||||||
|
|
||||||
|
// Update Docker network chart
|
||||||
|
if (data.docker.totalNetwork) {
|
||||||
|
const networkData = {
|
||||||
|
time: data.docker.totalNetwork.time || Math.floor(Date.now() / 1000),
|
||||||
|
in: parseFloat(data.docker.totalNetwork.received.value) * (data.docker.totalNetwork.received.unit === 'GB/s' ? 1e9 : data.docker.totalNetwork.received.unit === 'MB/s' ? 1e6 : data.docker.totalNetwork.received.unit === 'KB/s' ? 1e3 : 1),
|
||||||
|
out: parseFloat(data.docker.totalNetwork.sent.value) * (data.docker.totalNetwork.sent.unit === 'GB/s' ? 1e9 : data.docker.totalNetwork.sent.unit === 'MB/s' ? 1e6 : data.docker.totalNetwork.sent.unit === 'KB/s' ? 1e3 : 1),
|
||||||
|
unit: data.docker.totalNetwork.received.unit
|
||||||
|
};
|
||||||
|
dockerNetHistory.push(networkData);
|
||||||
|
if (dockerNetHistory.length > MAX_POINTS) {
|
||||||
|
dockerNetHistory.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Netdata network and disk history
|
||||||
|
if (data.netdata.net?.length) {
|
||||||
|
const latestNet = data.netdata.net[data.netdata.net.length - 1];
|
||||||
|
netHistory.push({
|
||||||
|
time: latestNet.time,
|
||||||
|
received: Math.abs(latestNet.received),
|
||||||
|
sent: Math.abs(latestNet.sent)
|
||||||
|
});
|
||||||
|
if (netHistory.length > MAX_POINTS) {
|
||||||
|
netHistory.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.netdata.disk?.length) {
|
||||||
|
const latestDisk = data.netdata.disk[data.netdata.disk.length - 1];
|
||||||
|
diskHistory.push({
|
||||||
|
time: latestDisk.time,
|
||||||
|
in: Math.abs(latestDisk.in),
|
||||||
|
out: Math.abs(latestDisk.out)
|
||||||
|
});
|
||||||
|
if (diskHistory.length > MAX_POINTS) {
|
||||||
|
diskHistory.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update charts with smoothing and dynamic units
|
||||||
|
const defaultLabels = Array(MAX_POINTS).fill(0).map((_, i) => i);
|
||||||
|
const defaultData = Array(MAX_POINTS).fill(0);
|
||||||
|
|
||||||
|
if (charts.dockerNet) {
|
||||||
|
const maxVal = Math.max(...dockerNetHistory.map(d => Math.max(d.in, d.out)), 1);
|
||||||
|
const unitInfo = formatBytes(maxVal);
|
||||||
|
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
|
||||||
|
const inData = smoothData(dockerNetHistory.map(d => d.in / scale), SMOOTHING_WINDOW);
|
||||||
|
const outData = smoothData(dockerNetHistory.map(d => d.out / scale), SMOOTHING_WINDOW);
|
||||||
|
charts.dockerNet.data.labels = dockerNetHistory.length ? dockerNetHistory.map((_, i) => i) : defaultLabels;
|
||||||
|
charts.dockerNet.data.datasets[0].data = inData.length ? inData : defaultData;
|
||||||
|
charts.dockerNet.data.datasets[1].data = outData.length ? outData : defaultData;
|
||||||
|
charts.dockerNet.options.scales.y.title.text = unitInfo.unit;
|
||||||
|
charts.dockerNet.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charts.cpu) {
|
||||||
|
const userData = smoothData(data.netdata.cpu?.map(d => d.user) || [], SMOOTHING_WINDOW);
|
||||||
|
const systemData = smoothData(data.netdata.cpu?.map(d => d.system) || [], SMOOTHING_WINDOW);
|
||||||
|
charts.cpu.data.labels = data.netdata.cpu?.length ? data.netdata.cpu.map((_, i) => i) : defaultLabels;
|
||||||
|
charts.cpu.data.datasets[0].data = userData.length ? userData : defaultData;
|
||||||
|
charts.cpu.data.datasets[1].data = systemData.length ? systemData : defaultData;
|
||||||
|
charts.cpu.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charts.ram) {
|
||||||
|
const usedData = smoothData(data.netdata.ram?.map(d => d.used) || [], SMOOTHING_WINDOW);
|
||||||
|
const freeData = smoothData(data.netdata.ram?.map(d => d.free) || [], SMOOTHING_WINDOW);
|
||||||
|
charts.ram.data.labels = data.netdata.ram?.length ? data.netdata.ram.map((_, i) => i) : defaultLabels;
|
||||||
|
charts.ram.data.datasets[0].data = usedData.length ? usedData : defaultData;
|
||||||
|
charts.ram.data.datasets[1].data = freeData.length ? freeData : defaultData;
|
||||||
|
charts.ram.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charts.net) {
|
||||||
|
const maxVal = Math.max(...netHistory.map(d => Math.max(d.received, d.sent)), 1);
|
||||||
|
const unitInfo = formatBytes(maxVal);
|
||||||
|
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
|
||||||
|
const receivedData = smoothData(netHistory.map(d => d.received / scale), SMOOTHING_WINDOW);
|
||||||
|
const sentData = smoothData(netHistory.map(d => d.sent / scale), SMOOTHING_WINDOW);
|
||||||
|
charts.net.data.labels = netHistory.length ? netHistory.map((_, i) => i) : defaultLabels;
|
||||||
|
charts.net.data.datasets[0].data = receivedData.length ? receivedData : defaultData;
|
||||||
|
charts.net.data.datasets[1].data = sentData.length ? sentData : defaultData;
|
||||||
|
charts.net.options.scales.y.title.text = unitInfo.unit;
|
||||||
|
charts.net.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charts.disk) {
|
||||||
|
const maxVal = Math.max(...diskHistory.map(d => Math.max(d.in, d.out)), 1);
|
||||||
|
const unitInfo = formatBytes(maxVal);
|
||||||
|
const scale = unitInfo.unit === 'GB/s' ? 1e9 : unitInfo.unit === 'MB/s' ? 1e6 : unitInfo.unit === 'KB/s' ? 1e3 : 1;
|
||||||
|
const inData = smoothData(diskHistory.map(d => d.in / scale), SMOOTHING_WINDOW);
|
||||||
|
const outData = smoothData(diskHistory.map(d => d.out / scale), SMOOTHING_WINDOW);
|
||||||
|
charts.disk.data.labels = diskHistory.length ? diskHistory.map((_, i) => i) : defaultLabels;
|
||||||
|
charts.disk.data.datasets[0].data = inData.length ? inData : defaultData;
|
||||||
|
charts.disk.data.datasets[1].data = outData.length ? outData : defaultData;
|
||||||
|
charts.disk.options.scales.y.title.text = unitInfo.unit;
|
||||||
|
charts.disk.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
updateUI(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket connected');
|
||||||
|
initCharts();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket disconnected');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
214
system-status.js
Normal file
214
system-status.js
Normal file
@ -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}`);
|
||||||
|
});
|
Reference in New Issue
Block a user