
🌐 Aussi en: English · Deutsch · Español
À partir de 17 heures, la nature passe en mode sombre – pour tout le système, sans interrupteur, sans possibilité de refus. Contrairement à ce blog, impossible de le désactiver d’un clic. 🌑 Un seul chiffre au thermomètre le matin, et les jours gris, ici dans le sud de l’Allemagne, les panneaux solaires produisent tout juste de quoi faire un café – un espresso, pas un cappuccino. ☕ Les pensées s’envolent vers les dernières vacances à Majorque : soleil, mer et la Playa de Palma. À la maison, la fête est plus calme : des serveurs au lieu de la sangria, des tableaux de bord au lieu de la piste de danse. 🍹
Place au sérieux.
Les serveurs, et tout particulièrement les applications web, tombent malades comme de vrais patients. Ils ont de la fièvre (CPU à fond), le souffle court (disque plein), des trous de mémoire (RAM qui s’épuise) ou simplement un mauvais jour où ils répondent plus lentement que d’habitude. Certains symptômes disparaissent d’eux-mêmes, d’autres sont les premiers signes de quelque chose de sérieux et doivent être examinés. C’est exactement cela, la supervision : prendre régulièrement le pouls, la température et la tension, repérer les anomalies avant qu’elles ne deviennent une urgence, et savoir où regarder quand ça devient grave.
Et comme chez le médecin : celui qui ne prend que son propre pouls ne remarque pas son propre arrêt cardiaque. C’était précisément la lacune qui restait dans la supervision.
🩺 Le bilan : qui vérifie quoi ?
Pour situer : il y a ici deux mondes. Sur Internet, trois serveurs loués – le blog et deux instances Nextcloud. À la maison, une petite baie avec mon propre matériel, dont le serveur NAS et de sauvegarde, qui tourne de toute façon jour et nuit et passe le plus clair de son temps à se tourner les pouces. Au fil des mois, des couches de supervision sont apparues dans les deux mondes. Chacune répond à une autre question.
Sur les trois serveurs sur Internet – chacun se surveille lui-même :
| Couche | Fréquence | Ce qu’elle remarque |
|---|---|---|
| Monit | toutes les 2 min | service arrêté, jail fail2ban disparue, bannissements et pare-feu désynchronisés, pod bloqué, certificat < 21 jours, sauvegarde trop ancienne, erreurs d’envoi de mail |
| Prometheus + Grafana | en continu | disque, RAM, CPU, conteneurs à leur limite, boucles de redémarrage, OOM kills, PHP-FPM saturé |
| Signaux de vie | en continu | une tâche nocturne (AIDE, rkhunter, ClamAV, mises à jour…) ne s’est pas manifestée |
| Loki | en continu | refus SELinux, erreurs fail2ban, OOM du noyau, un service qui écrit trop d’erreurs |
| Tests d’effet | à chaque exécution du playbook | chaîne de bannissement, filtres, domaines SELinux, métriques présentes |
Depuis le serveur NAS à la maison – le regard extérieur et sur mon propre matériel :
| Couche | Fréquence | Regarde | Ce qu’elle remarque |
|---|---|---|---|
| Analyse des attaques | quotidienne | les serveurs sur Internet | rafales de POST sur les points d’entrée PHP, plantages de PHP-FPM |
| Vue extérieure | hebdomadaire | les serveurs sur Internet | ports ouverts, certificats, versions, anciens serveurs oubliés |
| Températures | toutes les 10 min | uniquement la baie à la maison | historique thermique de mon propre matériel ; une vraie surchauffe est signalée par mail par l’iLO lui-même. Les serveurs sur Internet sont dans un centre de données – leur température est l’affaire de l’hébergeur. |
À retenir : chaque couche existe pour un type de panne. Monit voit si quelque chose tourne, Prometheus si cela suffit, Loki pourquoi ça coince, les signaux de vie si quelque chose s’est passé tout court.
🚑 La lacune : qui appelle le médecin quand le patient s’effondre ?
Le premier tableau le montre : tout ce qu’il contient tourne sur le serveur qu’il surveille. Si un serveur plante complètement, si l’hébergeur a une panne ou si le réseau disparaît, Monit, Grafana et Loki tombent avec lui. Personne n’envoie de mail – et dans la boîte de réception, le silence ressemble exactement à « tout va bien ». Un serveur non supervisé est le serveur de Schrödinger : à la fois en marche et en panne – jusqu’à ce que quelqu’un ouvre la boîte, le plus souvent un utilisateur, téléphone à la main. 🐈📦
Jusqu’ici, on l’aurait remarqué par le mail de test hebdomadaire qui n’arrive pas, ou par le contrôle extérieur du lundi. Au pire, après plusieurs jours. Il y a aussi un second cas : tous les services tournent, mais le site n’affiche que « Error establishing a database connection », ou Nextcloud reste bloqué en mode maintenance. Pour Monit, tout est vert. La question a d’ailleurs presque deux mille ans – Juvénal la posait dans ses Satires : Quis custodiet ipsos custodes ? Qui gardera les gardiens ?
Il s’agit de plus que de technique : je veux remarquer une panne moi-même avant que quelqu’un ne me contacte. C’est la différence entre réactif et proactif. Réactif signifie agir après qu’un problème est apparu – le plus souvent parce que quelqu’un appelle ou écrit « le cloud ne marche pas ». Le mal est alors fait, et l’utilisateur l’a vu avant moi. Proactif signifie faire en sorte, à l’avance, de repérer les problèmes soi-même et tôt – idéalement avant que quiconque ne les remarque, au minimum pour pouvoir répondre au premier message : « C’est connu, je m’en occupe. » Chaque couche des tableaux ci-dessus est un morceau de proactivité ; sans regard extérieur, justement la plus grosse panne restait réactive. Il existe deux sortes d’admins : ceux qui apprennent une panne par leur supervision – et ceux qui l’apprennent par leurs utilisateurs.
À retenir : une supervision qui tourne sur la même machine que le patient ne peut pas signaler sa panne totale. Il faut quelqu’un à l’extérieur.
🏠 La visite à domicile : un contrôle de disponibilité depuis la maison
La solution est volontairement petite et ne demande aucun nouveau matériel – le serveur NAS à la maison est de toute façon éveillé. Un script Python y appelle toutes les 5 minutes chacun des trois sites sur Internet comme le ferait un visiteur, simplement sans navigateur :
- WordPress : la page d’accueil répond 200 et c’est une vraie page, pas une page d’erreur de base de données ni un avis de maintenance.
- Nextcloud :
status.phpindique « installé », « pas en maintenance » et « aucune mise à jour de base de données en attente ». - Collabora :
/hosting/discoveryrenvoie le XML WOPI dont Nextcloud a besoin pour ouvrir des documents. - Certificat : une fois par jour – invalide ou valable moins de 14 jours, c’est un constat.
Un mail ne part que lors d’un changement : « panne » après deux exécutions ratées d’affilée (un raté isolé n’est pas une alarme), « de retour » avec la durée de la panne, et un rappel toutes les 12 heures tant que quelque chose reste en panne. Si toutes les cibles échouent en même temps, le script vérifie d’abord deux sites neutres sur Internet – s’ils sont aussi injoignables, c’est la propre connexion, et il n’y a pas de fausse alerte. En miniature, c’est le problème des deux généraux de l’informatique : quand aucune réponse ne revient, on ne sait pas si c’est le destinataire qui est tombé ou le messager – alors on demande à un tiers.
L’user agent contient « uptime », pour que les statistiques de visite ne comptent pas les 288 visites quotidiennes depuis mon propre salon. Et un clin d’œil à Heisenberg : une mesure doit modifier le moins possible ce qu’elle mesure – douze requêtes par heure se perdent dans le bruit de fond.
Sa propre limite : si le serveur NAS est éteint ou si la connexion de la maison tombe, personne ne vérifie. C’est malgré tout un grand pas – avant, le seul contrôle extérieur tournait une fois par semaine.
À retenir : vérifier depuis l’extérieur ce que voit l’utilisateur, pas si un processus tourne. Et signaler les changements d’état, pas les états – sinon les mails finissent vite non lus dans l’archive. Une alarme qui sonne sans arrêt, c’est un détecteur de fumée dans un sauna. 🔥
💓 Prendre la tension : ce que dit « pressure »
Autre nouveauté du tableau de bord, une valeur longtemps restée vide : pressure (Pressure Stall Information, PSI). Elle ne mesure pas à quel point une ressource est pleine, mais combien de temps les programmes ont dû attendre parce qu’elle manquait. Un CPU peut être occupé à 100 % sans que personne n’attende – c’est l’attente qui rend un serveur lent. À la caisse du supermarché, personne ne se soucie de savoir si la caissière est occupée – seulement de la longueur de la file.
$ 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 : attente de temps de calcul, de mémoire libre (le noyau a d’abord dû faire de la place – le premier signe avant des OOM kills) ou du disque.
- some : part du temps pendant laquelle au moins un processus attendait.
- full : part du temps pendant laquelle tous attendaient et rien n’avançait – la valeur la plus sérieuse.
- avg10 / avg60 / avg300 : pourcentage du temps, en moyenne sur 10 secondes, 1 et 5 minutes.
Règle empirique : quelques pour cent, c’est normal. some durablement au-dessus de 10–20 % se ressent ; full sur la mémoire ou le disque nettement au-dessus de 0 pendant un moment est un constat.
Une erreur de ma part au passage : sur ces serveurs, PSI est désactivé par défaut dans le noyau et s’active avec le paramètre de démarrage psi=1. Le poser a été rapide – mais il ne prend effet qu’au démarrage suivant. Le blog a redémarré la nuit suivante ; les deux serveurs Nextcloud ont tourné encore un jour et demi avec un panneau vide, jusqu’à un redémarrage manuel.
À retenir : un paramètre de démarrage ne compte qu’après le démarrage. Une modification n’est terminée que lorsque la valeur arrive là où l’on veut la voir.
🔍 Un constat pendant le bilan
En passant tout en revue, une autre chose est apparue : le script de températures de la baie à la maison – il ne mesure que mon propre matériel, pas les serveurs sur Internet – n’a pas d’envoi de mail configuré. La première inquiétude : personne ne signalerait une surchauffe. Un coup d’œil dans l’iLO a montré que son propre AlertMail était configuré depuis longtemps et signale lui-même les températures critiques. Le script n’ajoute que les premiers signes discrets dans son rapport.
À retenir : avant de construire une nouvelle alerte, regarder ce qui alerte déjà. Les alarmes en double ne servent à rien.
🔭 La suite : plus précis plutôt que plus
Une supervision de ce genre n’est jamais terminée. La faire évoluer ne veut pas dire ici ajouter de nouveaux outils – mais rendre les tests existants plus précis et plus spécifiques. Quelques pistes réalistes, qui ne demandent aucune nouvelle infrastructure – des idées, pas des promesses :
- La fonction plutôt que la disponibilité : un compte de test se connecte à Nextcloud, envoie un petit fichier via WebDAV, le récupère et compare la somme de contrôle. Cela vérifie d’un coup la connexion, le stockage, la base de données et Redis.
- Le contenu plutôt que le code d’état : sur le blog, un test ouvre un article précis et attend son titre – une page peut répondre 200 et être pourtant vide ou sortir d’un vieux cache.
- Lent avant cassé : enregistrer le temps de réponse et alerter quand il dépasse nettement sa propre médiane des derniers jours. La plupart des pannes s’annoncent d’abord par de la lenteur.
- Contrôle mutuel : les trois serveurs sur Internet se vérifient entre eux – ainsi quelqu’un reste éveillé même quand la connexion de la maison tombe.
- Qui contrôle la visite à domicile ? Le contrôle lui-même a besoin d’un signal de vie, par exemple un horodatage qu’une supervision sur Internet remarque quand il n’arrive plus. Juvénal vous salue.
S’y ajoute le réglage fin : les seuils qui se déclenchent trop souvent ou trop rarement sont ajustés, et chaque vrai incident qu’aucun test n’a vu devient une question : quel test l’aurait trouvé ?
À retenir : une supervision mûrit à chaque incident. Plus un test est spécifique, plus il est proche de ce que fait réellement un utilisateur.
📜 Le script
Uniquement la bibliothèque standard de Python, mail via la commande locale mail (ici msmtp). La configuration avec les adresses et le destinataire se trouve à côté, montrée plus bas avec des valeurs d’exemple.
#!/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 configuration uptime_check.conf (valeurs d’exemple) :
# 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/
Et l’entrée de la crontab :
*/5 * * * * cd $HOME/uptime-check && python3 -I uptime_check.py >> cron.log 2>&1Pour essayer : --dry-run affiche seulement les mails, --status l’état enregistré. Le plus simple pour tester une panne est une cible supplémentaire vers une adresse qui n’existe pas (.invalid) – pas des chemins erronés sur son propre serveur, sinon fail2ban bannit sa propre connexion.
🎯 Les leçons
- Une supervision sur le serveur lui-même ne peut pas signaler sa panne totale – quelqu’un doit regarder de l’extérieur.
- Vérifier ce que voit l’utilisateur : une vraie page, un « installed: true » – pas seulement un processus qui tourne.
- Signaler les changements d’état, pas les états ; deux échecs avant l’alarme filtrent les ratés.
- Pressure montre l’attente, pas l’utilisation – ce que les utilisateurs perçoivent comme « lent ».
- Un paramètre de démarrage n’agit qu’après le démarrage ; c’est fini seulement quand la valeur arrive.
Et maintenant : chauffage allumé, bouilloire en route, et encore un coup d’œil aux photos de Majorque. Les serveurs appelleront quand il faudra le médecin. There’s no place like 127.0.0.1 – mais c’est justement 127.0.0.1 qui ne peut prévenir personne quand il est lui-même en panne. C’est désormais le rôle de la visite à domicile. 😉





