Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`).
Expand Down
126 changes: 109 additions & 17 deletions agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
app.run(host='0.0.0.0', port=8080)
Loading