From 8cfa5ccfba0f5be823d8a246d854ac3726e78b2d Mon Sep 17 00:00:00 2001 From: Alan Lam <68211972+lam0819@users.noreply.github.com> Date: Tue, 6 May 2025 15:15:51 +0800 Subject: [PATCH 1/3] Add dashboard --- dashboard.html | 732 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 732 insertions(+) create mode 100644 dashboard.html diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..242e732 --- /dev/null +++ b/dashboard.html @@ -0,0 +1,732 @@ + + + + + + Simple Docker Agent Dashboard + + + + +
+

Simple Docker Agent Dashboard

+
+
+
Uptime
+
--
+
+
+
CPU Idle %
+
--
+
+
+
CPU Cores
+
--
+
+
+
Memory (used/total)
+
--
+
+
+
Swap (used/total)
+
--
+
+
+
+ Storage (used/total): -- +
+ + +
+
+
System Load Avg / Running / Blocked / Interrupts
+
1min, 5min, 15min load average; running/block IO procs; interrupts
+ +
+
+
CPU Mode Usage %
+
guest, idle, iowait, irq, nice, softirq, steal, system, user
+ +
+
+
Memory Usage Distribution (MB)
+
used, free, buffers, cached
+ +
+
+
Disk IO (Bps)
+
Read/write Bps and IO time (ms)
+ +
+
+
Network Usage (Bps per device)
+
Inbound/outbound Bps, per network device
+ +
+
+
Swap Usage (MB) and Activity (Bps)
+
Swap used/free; swap in/out speed
+ +
+
+ + +
+ + +
+
+
+
+
+
Container CPU %
+ +
+
+
Container Memory (MB)
+ +
+
+
+
+ + + + From 288e5e91b91c10ff45190ff68b6c296a145d09e1 Mon Sep 17 00:00:00 2001 From: Alan Lam <68211972+lam0819@users.noreply.github.com> Date: Tue, 6 May 2025 15:16:49 +0800 Subject: [PATCH 2/3] update agent.py to support dashboard --- agent.py | 126 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 17 deletions(-) diff --git a/agent.py b/agent.py index dcd9b91..ff5191c 100644 --- a/agent.py +++ b/agent.py @@ -2,31 +2,119 @@ import docker import humanize import concurrent.futures -from flask import Flask, jsonify +from flask import Flask, jsonify, send_from_directory from datetime import datetime, timezone +import os app = Flask(__name__) def get_host_resources(): + # Uptime + boot_time = psutil.boot_time() + uptime_seconds = int(datetime.now(timezone.utc).timestamp() - boot_time) + # Load average + load1, load5, load15 = psutil.getloadavg() + # CPU + cpu_times = psutil.cpu_times_percent(interval=0) + cpu_count = psutil.cpu_count() + cpu_percent = psutil.cpu_percent(interval=0) + # Memory mem = psutil.virtual_memory() + swap = psutil.swap_memory() + # Disk disk = psutil.disk_usage('/') - cpu_percent = psutil.cpu_percent(interval=0) # non-blocking, instantaneous + # IO + disk_io = psutil.disk_io_counters() + net_io = psutil.net_io_counters(pernic=True) + # Processes + procs = list(psutil.process_iter(['status'])) + running = sum(1 for p in procs if p.info['status'] == psutil.STATUS_RUNNING) + blocked = sum(1 for p in procs if p.info['status'] == psutil.STATUS_DISK_SLEEP) + # Interrupts (if available) + interrupts = None + try: + with open("/proc/stat") as f: + for line in f: + if line.startswith("intr"): + interrupts = int(line.split()[1]) + except Exception: + interrupts = None + return { - 'cpu_percent': cpu_percent, - 'memory': { - 'total': mem.total, - 'used': mem.used, - 'percent': mem.percent, - 'total_human': humanize.naturalsize(mem.total), - 'used_human': humanize.naturalsize(mem.used) + "uptime_seconds": uptime_seconds, + "uptime_human": humanize.precisedelta(uptime_seconds), + "boot_time": datetime.fromtimestamp(boot_time, tz=timezone.utc).isoformat(), + "cpu": { + "percent": cpu_percent, + "idle_percent": cpu_times.idle, + "count": cpu_count, + "times": cpu_times._asdict() + }, + "memory": { + "total": mem.total, + "available": mem.available, + "used": mem.used, + "free": mem.free, + "percent": mem.percent, + "buffers": getattr(mem, "buffers", 0), + "cached": getattr(mem, "cached", 0), + "total_human": humanize.naturalsize(mem.total), + "used_human": humanize.naturalsize(mem.used), + "available_human": humanize.naturalsize(mem.available), + "buffers_human": humanize.naturalsize(getattr(mem, "buffers", 0)), + "cached_human": humanize.naturalsize(getattr(mem, "cached", 0)), + }, + "swap": { + "total": swap.total, + "used": swap.used, + "free": swap.free, + "percent": swap.percent, + "sin": swap.sin, + "sout": swap.sout, + "total_human": humanize.naturalsize(swap.total), + "used_human": humanize.naturalsize(swap.used), + "free_human": humanize.naturalsize(swap.free), + }, + "disk": { + "total": disk.total, + "used": disk.used, + "free": disk.free, + "percent": disk.percent, + "total_human": humanize.naturalsize(disk.total), + "used_human": humanize.naturalsize(disk.used), + "free_human": humanize.naturalsize(disk.free) }, - 'disk': { - 'total': disk.total, - 'used': disk.used, - 'percent': disk.percent, - 'total_human': humanize.naturalsize(disk.total), - 'used_human': humanize.naturalsize(disk.used) - } + "disk_io": { + "read_bytes": disk_io.read_bytes, + "write_bytes": disk_io.write_bytes, + "read_count": disk_io.read_count, + "write_count": disk_io.write_count, + "read_time": getattr(disk_io, 'read_time', None), + "write_time": getattr(disk_io, 'write_time', None) + }, + "net_io": { + dev: { + "bytes_sent": val.bytes_sent, + "bytes_recv": val.bytes_recv, + "packets_sent": val.packets_sent, + "packets_recv": val.packets_recv, + "errin": val.errin, + "errout": val.errout, + "dropin": val.dropin, + "dropout": val.dropout + } + for dev, val in net_io.items() + }, + "loadavg": { + "1min": load1, + "5min": load5, + "15min": load15 + }, + "processes": { + "running": running, + "blocked_io": blocked + }, + "interrupts": interrupts } def calculate_cpu_percent(stat): @@ -134,5 +222,9 @@ def status(): 'docker_services': get_docker_services() }) +@app.route('/dashboard.html') +def dashboard(): + return send_from_directory(os.path.dirname(os.path.abspath(__file__)), 'dashboard.html') + if __name__ == "__main__": - app.run(host='0.0.0.0', port=8080) \ No newline at end of file + app.run(host='0.0.0.0', port=8080) From 5fc8ecfeab0a5cbde022cf702c537b79d3a90514 Mon Sep 17 00:00:00 2001 From: Alan Lam <68211972+lam0819@users.noreply.github.com> Date: Tue, 6 May 2025 15:18:58 +0800 Subject: [PATCH 3/3] Update README.md --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index 6befbc6..c3055fa 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ A lightweight Python-based agent that provides real-time monitoring of Docker co - **Fast & Lightweight**: Designed for quick responses, even with many containers. - **Easy Deployment**: Runs as a Docker container with minimal configuration. +## Screenshot + +![image](https://github.com/user-attachments/assets/b7a1f2dc-6f58-4edc-bb29-ba92002c26a5) +![image](https://github.com/user-attachments/assets/3960da14-9f7a-4b45-8e46-90d84ddcbb21) + + --- ## Quick Start @@ -136,6 +142,43 @@ curl http://localhost:8080/status | jq --- +## Web Dashboard + +A modern, interactive dashboard UI is included! Just open: + + http://localhost:8080/dashboard.html + +in your browser after starting the agent. No extra setup is required. + +### Dashboard Features +- **System Overview**: Uptime, CPU idle %, core count, memory, swap, and disk usage cards. +- **Charts**: Real-time graphs for system load, CPU modes, memory, disk IO, network, and swap. +- **Container Selector**: Switch between running containers to view their stats. +- **Per-Container Stats**: Status, image, uptime, restart count, ports, and resource usage (CPU %, memory MB) with historical charts. +- **Responsive Design**: Works on desktop and mobile. + +--- + +## API + +- **/status**: Returns a JSON object with host and container stats. See example above for structure. +- **/dashboard.html**: Serves the dashboard UI. + +--- + +## Running Without Docker (Advanced) + +You can run the agent directly with Python 3.8+ (Linux recommended): + +```bash +pip install flask psutil docker humanize +python agent.py +``` + +Then visit http://localhost:8080/dashboard.html in your browser. + +--- + ## Configuration - **Port**: Default is `8080` (see `agent.py`).