
🌐 También en: English · Deutsch · Français
A partir de las cinco de la tarde, la naturaleza pasa al modo oscuro – en todo el sistema, sin interruptor, sin opción de rechazo. A diferencia de este blog, no se puede quitar con un clic. 🌑 Un solo dígito por la mañana, y los días grises, aquí en el sur de Alemania, las placas solares dan justo para un café – un espresso, no un capuchino. ☕ Los pensamientos vuelan a las últimas vacaciones en Mallorca: sol, mar y la Playa de Palma. En casa la fiesta es más tranquila: servidores en lugar de sangría, paneles en lugar de pista de baile. 🍹
Ahora en serio.
Los servidores, y sobre todo las aplicaciones web, enferman como pacientes de verdad. Tienen fiebre (CPU al límite), les falta el aire (disco lleno), sufren lagunas de memoria (se agota la RAM) o simplemente tienen un mal día y responden más despacio que de costumbre. Algunos síntomas desaparecen solos, otros son los primeros indicios de algo serio y hay que investigarlos. Eso es exactamente la monitorización: tomar con regularidad el pulso, la temperatura y la tensión, detectar anomalías antes de que se conviertan en una urgencia y saber dónde mirar cuando la cosa se pone seria.
Y como con el médico: quien solo se toma su propio pulso no nota su propio paro cardíaco. Esa era exactamente la laguna que le quedaba a la monitorización aquí.
🩺 La revisión: ¿quién vigila qué?
Para situarse: aquí hay dos mundos. En Internet hay tres servidores alquilados – el blog y dos instancias de Nextcloud. En casa hay un pequeño rack con hardware propio, entre él el servidor NAS y de copias, que de todos modos funciona día y noche y pasa la mayor parte del tiempo sin hacer gran cosa. Con los meses han ido surgiendo capas de monitorización en ambos mundos. Cada una responde a una pregunta distinta.
En los tres servidores de Internet – cada uno se vigila a sí mismo:
| Capa | Frecuencia | Qué detecta |
|---|---|---|
| Monit | cada 2 min | servicio parado, jail de fail2ban desaparecida, bloqueos y cortafuegos desincronizados, pod atascado, certificado < 21 días, copia de seguridad demasiado antigua, errores de envío de correo |
| Prometheus + Grafana | continuamente | disco, RAM, CPU, contenedores en su límite, bucles de reinicio, OOM kills, PHP-FPM saturado |
| Señales de vida | continuamente | una tarea nocturna (AIDE, rkhunter, ClamAV, actualizaciones…) no ha dado señales |
| Loki | continuamente | denegaciones de SELinux, errores de fail2ban, OOM del kernel, un servicio que escribe demasiados errores |
| Pruebas de efecto | en cada ejecución del playbook | cadena de bloqueo, filtros, dominios SELinux, métricas presentes |
Desde el servidor NAS de casa – la mirada desde fuera y sobre el hardware propio:
| Capa | Frecuencia | Mira a | Qué detecta |
|---|---|---|---|
| Análisis de ataques | diario | los servidores de Internet | ráfagas de POST contra puntos de entrada PHP, caídas de PHP-FPM |
| Vista exterior | semanal | los servidores de Internet | puertos abiertos, certificados, versiones, servidores antiguos olvidados |
| Temperaturas | cada 10 min | solo el rack de casa | historial térmico del hardware propio; un sobrecalentamiento real lo avisa el propio iLO por correo. Los servidores de Internet están en un centro de datos – su temperatura es cosa del proveedor. |
Lo que queda: cada capa existe para un tipo de fallo. Monit ve si algo funciona, Prometheus si da abasto, Loki por qué se atasca, las señales de vida si ha pasado algo en absoluto.
🚑 La laguna: ¿quién llama al médico cuando el paciente se desploma?
La primera tabla lo delata: todo lo que contiene se ejecuta en el servidor que vigila. Si un servidor se cae del todo, el proveedor tiene una avería o la red desaparece, Monit, Grafana y Loki caen con él. Nadie envía un correo – y en la bandeja de entrada el silencio se parece exactamente a «todo bien». Un servidor sin vigilancia es el servidor de Schrödinger: encendido y caído a la vez – hasta que alguien abre la caja, normalmente un usuario con el teléfono en la mano. 🐈📦
Hasta ahora se habría notado por el correo de prueba semanal que no llega, o por la comprobación exterior del lunes. En el peor caso, al cabo de días. Hay además un segundo caso: todos los servicios funcionan, pero la web solo muestra «Error establishing a database connection», o Nextcloud se queda en modo mantenimiento. Para Monit todo está en verde. Por cierto, la pregunta tiene casi dos mil años – Juvenal la planteó en sus Sátiras: Quis custodiet ipsos custodes? ¿Quién vigila a los vigilantes?
Aquí se trata de algo más que técnica: quiero darme cuenta de una caída yo mismo antes de que alguien me avise. Esa es la diferencia entre reactivo y proactivo. Reactivo significa actuar después de que un problema se ha hecho visible – casi siempre porque alguien llama o escribe «la nube no funciona». Para entonces el daño ya está hecho, y el usuario lo vio antes que yo. Proactivo significa asegurarse de antemano de detectar los problemas uno mismo y pronto – idealmente antes de que nadie los note, como mínimo para poder responder al primer mensaje: «Lo sé, estoy en ello.» Cada capa de las tablas anteriores es un trozo de proactividad; sin una mirada desde fuera, precisamente la caída más grave seguía siendo reactiva. Hay dos tipos de administradores: los que se enteran de una caída por su monitorización – y los que se enteran por sus usuarios.
Lo que queda: una monitorización que corre en la misma máquina que el paciente no puede avisar de su caída total. Para eso hace falta alguien desde fuera.
🏠 La visita a domicilio: una comprobación de disponibilidad desde casa
La solución es pequeña a propósito y no necesita hardware nuevo – el servidor NAS de casa está despierto de todos modos. Un script de Python abre allí, cada 5 minutos, cada una de las tres webs de Internet como lo haría un visitante, solo que sin navegador:
- WordPress: la portada responde 200 y es una página real, no una página de error de base de datos ni un aviso de mantenimiento.
- Nextcloud:
status.phpindica «instalado», «sin modo mantenimiento» y «ninguna actualización de base de datos pendiente». - Collabora:
/hosting/discoverydevuelve el XML WOPI que Nextcloud necesita para abrir documentos. - Certificado: una vez al día – inválido o con menos de 14 días de validez es un hallazgo.
Solo se envía correo cuando hay un cambio: «caída» tras dos ejecuciones fallidas seguidas (un fallo aislado no es una alarma), «de vuelta» con la duración de la caída, y un recordatorio cada 12 horas mientras algo siga caído. Si fallan todos los objetivos a la vez, el script comprueba primero dos webs neutrales de Internet – si tampoco responden, el problema es la propia conexión y no hay falsa alarma. En pequeño, es el problema de los dos generales de la informática: si no llega respuesta, no se sabe si cayó el destinatario o el mensajero – así que se pregunta a un tercero.
El user agent contiene «uptime», para que las estadísticas de visitas no cuenten las 288 visitas diarias desde mi propio salón. Y un guiño a Heisenberg: una medición debe alterar lo menos posible lo que mide – doce peticiones por hora se pierden en el ruido de fondo.
Su propio límite: si el servidor NAS está apagado o se cae la conexión de casa, nadie comprueba nada. Aun así es un gran paso – antes, la única comprobación exterior se hacía una vez por semana.
Lo que queda: comprueba desde fuera lo que ve el usuario, no si un proceso está en marcha. Y avisa de cambios de estado, no de estados – si no, los correos acaban pronto sin leer en el archivo. Una alarma que suena sin parar es un detector de humo en una sauna. 🔥
💓 Tomar la tensión: qué significa «pressure»
También es nuevo en el panel un valor que estuvo mucho tiempo vacío: pressure (Pressure Stall Information, PSI). No mide lo lleno que está algo, sino cuánto tiempo tuvieron que esperar los programas porque faltaba un recurso. Una CPU puede estar ocupada al 100 % sin que nadie espere – lo que hace lento a un servidor es la espera. Al fin y al cabo, en la caja del supermercado a nadie le importa lo ocupada que esté la cajera – sino lo larga que es la cola.
$ cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.00 total=31
full avg10=0.00 avg60=0.00 avg300=0.00 total=31- cpu / memory / io: espera de tiempo de cálculo, de memoria libre (el kernel tuvo que liberar antes – la primera señal antes de los OOM kills) o del disco.
- some: parte del tiempo en que al menos un proceso esperaba.
- full: parte del tiempo en que todos esperaban y nada avanzaba – el valor más serio.
- avg10 / avg60 / avg300: porcentaje del tiempo, como media de 10 segundos, 1 y 5 minutos.
Regla práctica: unos pocos por ciento son normales. some por encima del 10–20 % de forma continuada se nota; full en memoria o disco claramente por encima de 0 durante un rato es un hallazgo.
Un error propio de paso: en estos servidores PSI viene desactivado en el kernel y se activa con el parámetro de arranque psi=1. Ponerlo fue rápido – pero solo surte efecto en el siguiente arranque. El blog reinició la noche siguiente; los dos servidores Nextcloud siguieron un día y medio más con el panel vacío, hasta que se reiniciaron a mano.
Lo que queda: un parámetro de arranque solo cuenta después del arranque. Un cambio no está terminado hasta que el valor llega donde uno quiere verlo.
🔍 Un hallazgo durante la revisión
Al repasarlo todo apareció algo más: el script de temperaturas del rack de casa – solo mide el hardware propio, no los servidores de Internet – no tiene configurado el envío de correo. La primera preocupación fue que nadie avisaría de un sobrecalentamiento. Un vistazo al iLO mostró que su propio AlertMail estaba configurado desde hacía tiempo y avisa por sí mismo de temperaturas críticas. El script solo añade las primeras señales discretas en su informe.
Lo que queda: antes de crear una alarma nueva, mira qué avisa ya. Las alarmas duplicadas no aportan nada.
🔭 Lo que viene: más preciso, no más
Una monitorización así nunca está terminada. Pero seguir desarrollándola no significa aquí añadir herramientas nuevas, sino hacer más precisas y específicas las pruebas que ya existen. Algunas ideas realistas que no necesitan infraestructura nueva – ideas, no promesas:
- Función en lugar de disponibilidad: una cuenta de prueba inicia sesión en Nextcloud, sube un archivo pequeño por WebDAV, lo vuelve a descargar y compara la suma de comprobación. Así se comprueban de una vez el inicio de sesión, el almacenamiento, la base de datos y Redis.
- Contenido en lugar de código de estado: en el blog, una prueba abre una entrada concreta y espera su título – una página puede responder 200 y aun así estar vacía o salir de una caché antigua.
- Lento antes que roto: registrar el tiempo de respuesta y avisar cuando supere claramente su propia mediana de los últimos días. La mayoría de las caídas se anuncian primero como lentitud.
- Control mutuo: los tres servidores de Internet se comprueban entre sí – así alguien sigue despierto aunque se caiga la conexión de casa.
- ¿Quién vigila la visita a domicilio? La propia comprobación necesita una señal de vida, por ejemplo una marca de tiempo que una monitorización en Internet echa de menos cuando deja de llegar. Saludos de Juvenal.
A eso se suma el ajuste fino: los umbrales que saltan demasiado o demasiado poco se corrigen, y cada incidente real que ninguna prueba detectó se convierte en una pregunta: ¿qué prueba lo habría encontrado?
Lo que queda: la monitorización madura con cada incidente. Cuanto más específica es una prueba, más se acerca a lo que hace de verdad un usuario.
📜 El script
Solo la biblioteca estándar de Python, correo mediante el comando local mail (aquí msmtp). La configuración con las direcciones y el destinatario va aparte, abajo con valores de ejemplo.
#!/usr/bin/env python3
"""
uptime_check.py - is every site still reachable and working, seen from outside?
Monit, Prometheus, Grafana and Loki run on the servers they watch. If a whole
server goes down (crash, hoster outage, network), they go down with it and
nobody sends a mail. This script runs on a machine at home every 5 minutes,
checks each site the way a visitor would and mails only when the state
changes: DOWN after <fail_after> failed runs in a row, UP again with the
downtime, a reminder every <remind_hours> while it stays down.
uptime_check.py run all checks (cron)
uptime_check.py --dry-run run all checks, print mails instead of sending
uptime_check.py --status show the stored state
Kinds of checks (uptime_check.conf, [targets]):
wordpress page answers 200 and is a real page, not a database error
nextcloud status.php: installed, not in maintenance, no pending DB upgrade
collabora /hosting/discovery answers with the WOPI discovery XML
Every target also gets a TLS certificate check (warning below cert_warn_days).
Standard library only. Mail goes through the local "mail" command (msmtp).
"""
import configparser, datetime as dt, json, os, socket, ssl, subprocess, sys, time
import urllib.error, urllib.request
from urllib.parse import urlsplit
HERE = os.path.dirname(os.path.abspath(__file__))
CONF = os.path.join(HERE, "uptime_check.conf")
STATE = os.path.join(HERE, "state.json")
LOG = os.path.join(HERE, "uptime_check.log")
UA = "uptime-check/1.0 (+homelab)" # "uptime" keeps it out of the visitor statistics
def log(msg):
with open(LOG, "a") as f:
f.write(f"{dt.datetime.now():%Y-%m-%d %H:%M:%S} {msg}\n")
def fetch(url, timeout):
"""GET url -> (status, body text, seconds). Raises on network errors."""
req = urllib.request.Request(url, headers={"User-Agent": UA})
t0 = time.monotonic()
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
body = r.read(2_000_000).decode("utf-8", "replace")
return r.status, body, time.monotonic() - t0
except urllib.error.HTTPError as e:
return e.code, "", time.monotonic() - t0
# Each check returns None when everything is fine, otherwise a short reason.
def check_wordpress(base, timeout):
status, body, secs = fetch(base, timeout)
if status != 200:
return f"HTTP {status}"
if "Error establishing a database connection" in body:
return "WordPress meldet: keine Datenbankverbindung"
if "Briefly unavailable for scheduled maintenance" in body:
return "WordPress steckt im Wartungsmodus"
if "wp-content" not in body or "</html>" not in body:
return "Antwort ist keine vollständige WordPress-Seite"
return None
def check_nextcloud(base, timeout):
status, body, secs = fetch(base.rstrip("/") + "/status.php", timeout)
if status != 200:
return f"status.php: HTTP {status}"
try:
s = json.loads(body)
except ValueError:
return "status.php liefert kein JSON"
if not s.get("installed"):
return "status.php: installed = false"
if s.get("maintenance"):
return "Nextcloud ist im Wartungsmodus"
if s.get("needsDbUpgrade"):
return "Nextcloud wartet auf ein Datenbank-Upgrade (occ upgrade)"
return None
def check_collabora(base, timeout):
status, body, secs = fetch(base.rstrip("/") + "/hosting/discovery", timeout)
if status != 200:
return f"discovery: HTTP {status}"
if "<wopi-discovery" not in body:
return "discovery liefert kein WOPI-XML"
return None
CHECKS = {"wordpress": check_wordpress, "nextcloud": check_nextcloud, "collabora": check_collabora}
def cert_days_left(base, timeout):
host = urlsplit(base).hostname
ctx = ssl.create_default_context() # also fails on an invalid or wrong-name certificate
with socket.create_connection((host, 443), timeout=timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls:
not_after = ssl.cert_time_to_seconds(tls.getpeercert()["notAfter"])
return int((not_after - time.time()) // 86400)
def run_check(kind, base, timeout):
try:
return CHECKS[kind](base, timeout)
except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError, ssl.SSLError, OSError) as e:
reason = getattr(e, "reason", e)
return f"nicht erreichbar ({reason})"
def online(refs, timeout):
"""Is this machine itself online? True if any reference URL answers at all."""
for url in refs:
try:
fetch(url, timeout)
return True
except Exception:
continue
return False
def fmt_duration(seconds):
m = int(seconds // 60)
return f"{m // 60} h {m % 60} min" if m >= 60 else f"{m} min"
def send_mail(to, subject, text, dry_run):
if dry_run:
print(f"--- MAIL an {to}: {subject}\n{text}\n---")
return
r = subprocess.run(["mail", "-s", subject, to], input=text, text=True, capture_output=True)
if r.returncode != 0:
log(f"MAIL FEHLER rc={r.returncode}: {r.stderr.strip()}")
def main():
dry_run = "--dry-run" in sys.argv
cfg = configparser.ConfigParser()
if not cfg.read(CONF):
sys.exit(f"config missing: {CONF}")
g = cfg["general"]
timeout = g.getint("timeout", 20)
fail_after = g.getint("fail_after", 2)
remind = g.getint("remind_hours", 12) * 3600
cert_warn = g.getint("cert_warn_days", 14)
refs = g.get("reference_urls", "").split()
mail_to = g["mail_to"]
try:
state = json.load(open(STATE))
except (OSError, ValueError):
state = {}
if "--status" in sys.argv:
print(json.dumps(state, indent=2, ensure_ascii=False))
return
now = int(time.time())
targets = {name: val.split(None, 1) for name, val in cfg["targets"].items()}
results = {name: run_check(kind, url, timeout) for name, (kind, url) in targets.items()}
# Everything failed at once? Then it is more likely this machine's own
# line than all servers - check a neutral reference before alarming.
if all(results.values()) and not online(refs, timeout):
log("alle Ziele fehlgeschlagen und Referenz nicht erreichbar - eigene Leitung offline, nichts gezählt")
return
mails = []
for name, (kind, url) in targets.items():
st = state.setdefault(name, {"status": "up", "fails": 0, "since": now, "last_mail": 0})
reason = results[name]
if reason is None:
if st["status"] == "down":
mails.append(f"WIEDER DA {name} ({url}) nach {fmt_duration(now - st['since'])}")
log(f"UP {name} nach {fmt_duration(now - st['since'])}")
st.update(status="up", since=now)
st["fails"] = 0
else:
st["fails"] += 1
log(f"FAIL {name} ({st['fails']}x): {reason}")
if st["status"] == "up" and st["fails"] >= fail_after:
st.update(status="down", since=now, last_mail=now, reason=reason)
mails.append(f"AUSFALL {name} ({url}): {reason}")
elif st["status"] == "down" and now - st["last_mail"] >= remind:
st["last_mail"] = now
mails.append(f"NOCH DOWN {name} ({url}) seit {fmt_duration(now - st['since'])}: {reason}")
# Certificate: checked once per target and day, so at most one warning a day.
if reason is None and now - st.get("cert_checked", 0) >= 86400:
st["cert_checked"] = now
try:
days = cert_days_left(url, timeout)
if days < cert_warn:
mails.append(f"ZERTIFIKAT {name}: läuft in {days} Tagen ab (Erneuerung hängt?)")
except Exception as e:
mails.append(f"ZERTIFIKAT {name}: Prüfung fehlgeschlagen ({e})")
if mails:
down = sum(1 for s in state.values() if s["status"] == "down")
subject = f"Erreichbarkeit: {down} Ziel(e) down" if down else "Erreichbarkeit: alles wieder erreichbar"
text = "\n".join(mails) + f"\n\nGeprüft {dt.datetime.now():%Y-%m-%d %H:%M} von {socket.gethostname()}.\n"
send_mail(mail_to, subject, text, dry_run)
if not dry_run:
tmp = STATE + ".tmp"
with open(tmp, "w") as f:
json.dump(state, f, indent=1)
os.replace(tmp, STATE)
if __name__ == "__main__":
main()
La configuración uptime_check.conf (valores de ejemplo):
# uptime_check.conf - chmod 600 (contains the mail address)
[general]
mail_to = you@example.org
# seconds per request
timeout = 20
# failed runs in a row before a DOWN mail (cron every 5 min -> 10 min)
fail_after = 2
remind_hours = 12
cert_warn_days = 14
# neutral sites: if ALL targets fail, these tell "server down" from "own line down"
reference_urls = https://www.google.com/generate_204 https://one.one.one.one/
[targets]
# name = kind base-url
blog = wordpress https://blog.example.org/
cloud = nextcloud https://cloud.example.org/
office = collabora https://office.example.org/
Y la entrada de la crontab:
*/5 * * * * cd $HOME/uptime-check && python3 -I uptime_check.py >> cron.log 2>&1Para probarlo: --dry-run solo muestra los correos, --status el estado guardado. Lo más fácil para probar una caída es un objetivo extra hacia una dirección que no existe (.invalid) – no rutas erróneas en tu propio servidor, o fail2ban bloqueará tu propia conexión.
🎯 Las lecciones
- La monitorización en el propio servidor no puede avisar de su caída total – alguien tiene que mirar desde fuera.
- Comprueba lo que ve el usuario: una página real, un «installed: true» – no solo un proceso en marcha.
- Avisa de cambios de estado, no de estados; dos fallos antes de la alarma filtran los tropiezos.
- Pressure muestra espera, no uso – lo que los usuarios perciben como «lento».
- Un parámetro de arranque solo funciona tras el arranque; está hecho cuando el valor llega.
Y ahora: calefacción encendida, agua al fuego y otro vistazo a las fotos de Mallorca. Los servidores llamarán cuando haga falta el médico. There’s no place like 127.0.0.1 – pero precisamente 127.0.0.1 no puede avisar a nadie de que está caído. Para eso está ahora la visita a domicilio. 😉





