📋 Contexto
Prometheus + Grafana es el stack estándar para monitoreo en DevOps, pero para un home lab con recursos limitados puede ser overkill. Prometheus consume ~200-400 MB RAM, Grafana otros ~200 MB, más configuración compleja y mantenimiento.
Para mi servidor (Lenovo G480 con 4GB RAM), quería algo más ligero: un dashboard simple que muestre CPU, RAM, disco, red, y estado de containers Docker, todo con menos de 20 MB de RAM.
🎯 Objetivo
- Monitoreo en tiempo real de recursos del sistema
- Dashboard terminal (TUI) para SSH
- Dashboard web accesible desde navegador
- Monitoreo de containers Docker
- Consumo mínimo de recursos (<20 MB RAM)
- Sin dependencias pesadas (no frameworks web)
🛠️ Implementación
Herramientas Utilizadas
psutil: Biblioteca Python para métricas del sistemacurses: TUI (Terminal User Interface) nativa de Pythondocker: Docker SDK para Pythonsystemd: Para ejecutar el monitor como servicio
Script 1: Monitor Terminal (sysmon.py)
Monitor interactivo para usar vía SSH:
#!/usr/bin/env python3
import psutil
import curses
import time
from datetime import datetime
def bytes_to_human(bytes_val):
"""Convierte bytes a formato legible"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_val < 1024.0:
return f"{bytes_val:.2f} {unit}"
bytes_val /= 1024.0
def draw_bar(stdscr, y, x, label, percent, color_pair=1):
"""Dibuja barra de progreso"""
bar_width = 50
filled = int(bar_width * percent / 100)
bar = '█' * filled + '░' * (bar_width - filled)
stdscr.addstr(y, x, f"{label:<20} [{bar}] {percent:.1f}%",
curses.color_pair(color_pair))
def main(stdscr):
# Configurar colores
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_BLACK)
curses.curs_set(0) # Ocultar cursor
stdscr.nodelay(1) # Non-blocking input
while True:
stdscr.clear()
y = 0
# Header
stdscr.addstr(y, 0, "═" * 70, curses.color_pair(4))
y += 1
stdscr.addstr(y, 0, " SysDevOps-Pro v2.1: Monitor Sistema + Docker/n8n",
curses.A_BOLD | curses.color_pair(4))
y += 1
stdscr.addstr(y, 0, f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
curses.color_pair(2))
y += 2
# CPU
cpu_percent = psutil.cpu_percent(interval=0.5)
color = 1 if cpu_percent > 80 else 3 if cpu_percent > 50 else 2
draw_bar(stdscr, y, 0, "CPU", cpu_percent, color)
y += 2
# RAM
mem = psutil.virtual_memory()
draw_bar(stdscr, y, 0, "RAM", mem.percent, 4)
y += 1
stdscr.addstr(y, 0,
f" Total: {bytes_to_human(mem.total)} | "
f"Usado: {bytes_to_human(mem.used)} | "
f"Disponible: {bytes_to_human(mem.available)}",
curses.color_pair(2))
y += 2
# Disco
disk = psutil.disk_usage('/')
draw_bar(stdscr, y, 0, "Disco (Root)", disk.percent, 1)
y += 1
stdscr.addstr(y, 0,
f" Total: {bytes_to_human(disk.total)} | "
f"Usado: {bytes_to_human(disk.used)} | "
f"Disponible: {bytes_to_human(disk.free)}",
curses.color_pair(2))
y += 2
# Network I/O
net = psutil.net_io_counters()
stdscr.addstr(y, 0, "Network I/O:", curses.A_BOLD)
y += 1
stdscr.addstr(y, 0,
f" ↑ Sent: {bytes_to_human(net.bytes_sent)} | "
f"↓ Recv: {bytes_to_human(net.bytes_recv)}",
curses.color_pair(2))
y += 2
# Uptime
boot_time = datetime.fromtimestamp(psutil.boot_time())
uptime = datetime.now() - boot_time
days, remainder = divmod(uptime.total_seconds(), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, _ = divmod(remainder, 60)
stdscr.addstr(y, 0,
f"Uptime: {int(days)}d {int(hours)}h {int(minutes)}m",
curses.color_pair(3))
y += 2
# Footer
stdscr.addstr(y, 0, "Presiona 'q' para salir", curses.color_pair(3))
stdscr.refresh()
# Check for quit
key = stdscr.getch()
if key == ord('q'):
break
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
• Multiplataforma (Linux, Windows, Mac)
• Lightweight (sin dependencias C pesadas)
• API simple y Pythonic
• Mantenido activamente
• Documentación excelente
Script 2: Monitor Web (sysmon_web.py)
Genera HTML estático que se actualiza cada 30 segundos:
#!/usr/bin/env python3
import psutil
import docker
import time
from datetime import datetime
UPDATE_INTERVAL = 30
OUTPUT_FILE = "/var/www/html/status.html"
def bytes_to_human(bytes_val):
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_val < 1024.0:
return f"{bytes_val:.2f} {unit}"
bytes_val /= 1024.0
def get_docker_stats():
try:
client = docker.from_env()
containers = client.containers.list()
stats = []
for container in containers:
stats.append({
'name': container.name,
'status': container.status,
'image': container.image.tags[0] if container.image.tags else 'none'
})
return stats
except:
return []
def generate_html():
cpu = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage('/')
net = psutil.net_io_counters()
boot_time = datetime.fromtimestamp(psutil.boot_time())
uptime = datetime.now() - boot_time
docker_containers = get_docker_stats()
html = f"""
Server Status - MyTechZone
⚡ Server Status
Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} UTC
● System Online
CPU Usage: {cpu:.1f}%
RAM: {mem.percent:.1f}% ({bytes_to_human(mem.used)} / {bytes_to_human(mem.total)})
Disk: {disk.percent:.1f}% ({bytes_to_human(disk.used)} / {bytes_to_human(disk.total)})
Docker Containers ({len(docker_containers)})
"""
for container in docker_containers:
status_class = "status-ok" if container['status'] == 'running' else "status-error"
html += f"- {container['name']} - {container['status']}
\n"
html += f"""
Network & Uptime
Total Sent ↑ {bytes_to_human(net.bytes_sent)}
Total Received ↓ {bytes_to_human(net.bytes_recv)}
System Uptime: {int(uptime.days)}d {int(uptime.seconds//3600)}h {int((uptime.seconds//60)%60)}m
← Back to Home
"""
return html
def main():
print(f"Starting sysmon_web...")
print(f"Output: {OUTPUT_FILE}")
print(f"Update interval: {UPDATE_INTERVAL}s")
while True:
try:
html = generate_html()
with open(OUTPUT_FILE, 'w') as f:
f.write(html)
print(f"[{datetime.now().strftime('%H:%M:%S')}] ✓ Status updated")
time.sleep(UPDATE_INTERVAL)
except KeyboardInterrupt:
print("\nStopping sysmon_web...")
break
except Exception as e:
print(f"Error: {e}")
time.sleep(UPDATE_INTERVAL)
if __name__ == "__main__":
main()
Servicio Systemd
Para que el monitor web corra automáticamente:
# /etc/systemd/system/sysmon-web.service
[Unit]
Description=SysDevOps Web Monitor
After=network.target docker.service
[Service]
Type=simple
User=mytechzone
WorkingDirectory=/home/mytechzone/scripts
ExecStart=/usr/bin/python3 /home/mytechzone/scripts/sysmon_web.py
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
# Habilitar y iniciar
sudo systemctl daemon-reload
sudo systemctl enable sysmon-web
sudo systemctl start sysmon-web
# Ver logs
sudo journalctl -u sysmon-web -f
📊 Comparación: Custom vs Prometheus+Grafana
| Aspecto | Custom (Python) | Prometheus+Grafana |
|---|---|---|
| RAM Consumida | ~15-20 MB | ~400-600 MB |
| Setup Time | 30 minutos | 2-3 horas |
| Métricas Históricas | ❌ No (solo actual) | ✅ Sí (configurable) |
| Alertas | ⚠️ Requiere script adicional | ✅ Built-in |
| Dashboards | ⚠️ Básico | ✅ Avanzado |
| Escalabilidad | ❌ Un servidor | ✅ Multi-servidor |
• Custom Python: Home labs, servidores únicos, recursos limitados
• Prometheus+Grafana: Producción, múltiples servidores, métricas históricas críticas
🚀 Mejoras Opcionales
1. Monitoreo de Temperatura del CPU ✅ Implementado (v2.0)
sysmon_web v2.0 y está corriendo en producción en mytechzone.dev/status.html
El monitoreo de temperatura es crucial para hardware antiguo (como mi Lenovo 2013 con Celeron B970). Implementé lectura desde /sys/class/thermal/ con fallback a psutil:
def get_cpu_temperature():
"""Obtener temperatura del CPU."""
try:
# Método 1: Leer desde /sys/class/thermal (más confiable)
thermal_zones = glob.glob('/sys/class/thermal/thermal_zone*/temp')
if thermal_zones:
temps = []
for zone in thermal_zones:
try:
with open(zone, 'r') as f:
# Convertir de milidegrees a degrees
temp = int(f.read().strip()) / 1000.0
temps.append(temp)
except:
continue
if temps:
return max(temps) # Retornar la más alta
# Método 2: Usar psutil (fallback)
temps = psutil.sensors_temperatures()
if temps:
for name, entries in temps.items():
if 'coretemp' in name.lower() or 'cpu' in name.lower():
if entries:
return entries[0].current
return None
except Exception as e:
return None
Sistema de alertas por color implementado:
- 🟢 < 60°C: OK (verde)
- 🟡 60-69°C: Warm (amarillo)
- 🟠 70-79°C: Warning (naranja)
- 🔴 ≥ 80°C: Critical (rojo)
Si la temperatura alcanza ≥ 75°C, el dashboard muestra automáticamente:
⚠️ High temperature - Consider opening lid for ventilation
Impacto en performance:
- CPU adicional: < 0.1%
- RAM adicional: ~1-2 KB
- I/O: 1 lectura cada 30 segundos
- Resultado: Impacto prácticamente nulo ✅
2. Alertas por Email
import smtplib
from email.message import EmailMessage
def send_alert(subject, body):
msg = EmailMessage()
msg['Subject'] = subject
msg['From'] = '[email protected]'
msg['To'] = '[email protected]'
msg.set_content(body)
with smtplib.SMTP('localhost') as s:
s.send_message(msg)
# Usar en el loop
if cpu_percent > 90:
send_alert("CPU Alert", f"CPU at {cpu_percent}%")
3. Métricas Históricas con SQLite
import sqlite3
def save_metrics(cpu, ram, disk):
conn = sqlite3.connect('metrics.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS metrics
(timestamp TEXT, cpu REAL, ram REAL, disk REAL)''')
c.execute("INSERT INTO metrics VALUES (datetime('now'), ?, ?, ?)",
(cpu, ram, disk))
conn.commit()
conn.close()
📚 Recursos Útiles
💡 Lecciones Aprendidas
- psutil es suficiente: Para 90% de casos de uso, no necesitas Prometheus.
- HTML estático es rápido: Generar HTML cada 30s es más eficiente que un servidor web con templating.
- Systemd hace el trabajo pesado: Auto-restart, logging, y startup automático gratis.
- Menos es más: 200 líneas de Python vs configurar 3 servicios complejos.
- Docker SDK es fácil: Integrar métricas de containers es trivial con docker-py.
✅ Resultado Final
Características (v2.0 - Enero 2026):
• Actualización cada 30 segundos
• CPU, RAM, Disco, Red, Uptime
• 🌡️ Temperatura del CPU con alertas por color (NUEVO)
• Estado de 4 containers Docker (nginx, n8n, postgres, cloudflared)
• Servicio systemd estable
• ~15-18 MB de RAM consumidos
• 0 downtime en 60+ días
• Warning automático si temperatura ≥ 75°C
💭 Conclusión
Para un home lab, un monitor custom con Python es perfecto: ligero, simple, y hace exactamente lo que necesitas sin overhead innecesario. No necesitas Prometheus+Grafana hasta que tengas múltiples servidores o requisitos de métricas históricas avanzadas.
Este proyecto me enseñó que a veces la mejor solución es la más simple. 200 líneas de Python bien escritas pueden reemplazar un stack complejo de monitoreo para casos de uso básicos.