Le problème : savoir que le site est tombé avant que le client ne l'écrive — y compris quand il répond 200 avec une page vide, un bouton « Payer » disparu au dernier déploiement, un port de base de données fermé, un enregistrement DNS effacé, un certificat expiré, ou une sauvegarde nocturne qui ne tourne plus.
Un contrôle ne regarde pas seulement le code de retour. C'est tout l'écart avec un surveillant classique : la panne la plus coûteuse répond 200.
Démarrer en 30 secondes
Sur /uptime, choisir un domaine du projet, un chemin, et — facultatif — un texte qui doit figurer dans la page :
- chemin
/paiement - texte attendu
Payer
C'est tout. Le contrôle part toutes les cinq minutes, alerte par e-mail après deux observations concordantes, et ne redit rien tant que la panne dure.
Pour surveiller un traitement qui n'a pas d'adresse — une sauvegarde de nuit — prendre le type battement de coeur, et coller la ligne rendue à la fin du script :
``bash pg_dump … && curl -fsS https://letock.fr/api/battement/LA_CLE ``
Le silence devient la panne : si l'appel n'arrive pas dans l'intervalle déclaré plus le délai de grâce, Tock alerte. C'est le seul montage qui détecte l'absence d'exécution.
Les cinq types de contrôle
| type | ce qu'il vise | champs propres |
|---|---|---|
http | une page ou une API. Le défaut | chemin, assertions, parcours |
tcp | un port ouvert : base de données, SMTP, file | cible (hôte), port (requis) |
dns | un enregistrement présent, et sa valeur | dnsType (A, AAAA, MX, TXT, NS, CNAME), dnsAttendu |
ssl | la date d'expiration d'un certificat | cible, port (443 par défaut) |
battement | l'appel que le client émet lui-même | graceMin, clé d'appel |
tcp et ssl exigent que le domaine ait été prouvé (page Veille) : ouvrir une prise vers un hôte choisi par l'appelant, sans preuve, serait un balayage de ports offert à tout inscrit. cible doit rester sous le domaine déclaré — smtp.monsite.fr oui, evil.example.com non.
ICMP (le « ping ») n'existe pas et n'existera pas. Un ping demande une prise brute, donc les privilèges de l'administrateur du système ; appeler le binaire ping donnerait une sonde qui marche sur une machine et pas sur l'autre. Il n'apprend d'ailleurs presque rien de plus qu'un TCP — la moitié des hébergeurs filtrent l'ICMP, et un serveur qui répond au ping avec toutes ses applications mortes est le cas normal.
Les assertions d'un contrôle HTTP
Toutes facultatives ; un contrôle qui n'en porte aucune vaut « statut inférieur à 400 ». Elles sont évaluées du transport vers le contenu, et le premier constat s'arrête là : une page qui répond 503 ne mérite pas qu'on lui reproche aussi l'absence de son bouton.
statutAttendu— le code exact exigé.attendu— un texte qui doit figurer dans la page.absent— un texte qui ne doit pas y figurer : « Erreur 500 », « maintenance ».enteteNom/enteteValeur— un en-tête attendu, et ce qu'il doit contenir.maxMs— un budget de temps. Le dépasser est une panne, pas une lenteur : un budget déclaré est une promesse tenue ou non.jsonChemin/jsonValeur— une valeur dans le JSON :etat,data.0.id. Les chemins sont volontairement pauvres — segments séparés par des points, entiers pour les tableaux. Personne ne surveille une API avec un filtre récursif.
Deux pièges d'écriture, rapportés par des clients et corrigés depuis.
Un attendu ou un absent contenant un caractère de remplacement (U+FFFD) est désormais refusé à l'écriture. Ce caractère est ce que produit un décodage raté : un accent abîmé avant d'arriver ici, presque toujours un terminal qui n'est pas en UTF-8. Sans ce refus, l'appel rendait 201, la donnée était écrite, et le défaut ne se manifestait qu'à la première sonde — sous la forme d'un contrôle qui ne trouverait jamais son texte, sur un site parfaitement sain. Si tu pilotes l'API depuis un terminal : écris la charge utile dans un fichier UTF-8 et envoie-la avec --data-binary "@fichier", jamais en argument.
Et `absent` ne doit jamais porter sur un texte qui vit dans un gabarit. Sur un site en Next.js App Router, la charge RSC sérialise les *slots* du gabarit — notFound, error, le squelette de chargement — dans le corps de toutes les pages. Le texte de ta 404 est donc présent dans chaque page sans y être à l'écran une seule fois, et c'est précisément celui qu'on a envie de mettre en absent. Un absent sûr est un texte qui n'existe que dans le rendu d'une page en échec : un code d'erreur applicatif, un identifiant de trace.
Un `PATCH` remplace le contrôle, il ne le complète pas. Les champs que tu ne renvoies pas reviennent à leur défaut. La réponse porte donc efface: ["attendu", "max_ms", "nom"] — ce qui est parti faute d'avoir été renvoyé — et dry_run: true te le montre avant d'écrire. Une création se relit ; une modification se croit.
Un parcours enchaîne jusqu'à cinq étapes sur le même domaine en gardant les cookies : accueil, connexion (POST avec un corps application/x-www-form-urlencoded), tableau de bord. C'est ce qui répond à « est-ce qu'on peut encore se connecter ».
Les routes
GET, POST ou HEAD /api/battement/{cle}
Publique, sans authentification, et c'est délibéré : cette adresse finit dans un curl d'une ligne écrit par quelqu'un qui n'ouvrira pas de documentation. La clé est dans le chemin, imprévisible, et le pire qu'un tiers la connaissant puisse faire est de taire une alerte — jamais d'en lire une ni d'en fabriquer une.
Les trois verbes comptent comme un battement : curl fait un GET, wget --post-data un POST, certains ordonnanceurs un HEAD.
Réponse 200 : { "ok": true, "retabli": false }. retabli: true quand cet appel referme une panne ouverte. 404 battement_inconnu pour une clé inconnue ou révoquée, 404 cle_invalide pour une clé mal formée. Limite : 120 appels par minute.
GET /statut/{cle}
La page de statut publique du projet, activée depuis /uptime. La clé rend l'adresse imprévisible sans être un secret — la page ne montre que ce que le client a choisi de publier : l'état de chaque contrôle, la frise des derniers relevés, les incidents avec leur message écrit à la main, et les maintenances déclarées.
Elle montre tous les contrôles du projet, y compris les battements et les contrôles de port. Leur libellé est celui de la cible (db.monsite.fr:5432, MX monsite.fr) ou le nom donné au contrôle pour un battement — jamais une adresse HTTP inventée. Un traitement interne qu'on ne veut pas publier ne doit donc pas vivre dans un projet dont la page de statut est publiée.
Les canaux d'alerte
L'e-mail est le canal par défaut et ne se supprime pas : il se déduit des membres de l'équipe. Cinq canaux en plus par projet :
- `webhook` — un
POSTJSON signé, en-têtetock-signature: t=…,v1=…, HMAC-SHA256 de"<t>.<corps>"(même vérification que les crochets de Mail). Charge utile :{ evenement: "panne"|"relance"|"retabli", controle_id, nom, cible, raison, duree_min, quand }. - `slack` — l'adresse doit commencer par
https://hooks.slack.com/. La charge est l'objet{ "text": … }que Slack sait lire ; le client n'a rien à fabriquer.
Sans file, sans réessai. Une alerte remise une heure plus tard n'a plus de valeur : la panne est finie et le message arrive comme un fantôme. On tente une fois, on note le code, et l'écran montre le compteur d'échecs. Après vingt échecs consécutifs, le canal se tait.
Ni SMS ni appel téléphonique : ce sont un contrat d'opérateur, une facturation à l'unité et une vérification de numéro par pays — une ligne de métier, pas une fonction. Un webhook branché sur un service d'astreinte fait la même chose, et le client choisit son prestataire.
Les écrans
/uptime— les contrôles du projet, l'ajout, les canaux d'alerte, la publication de la page de statut./uptime/{id}— un contrôle : ses réglages, ses étapes de parcours, ses derniers relevés, ses incidents. Pour un battement : la ligne à coller et la date du dernier appel reçu./uptime/incidents— les incidents du projet, ouverts d'abord, avec le message publiable sur la page de statut./uptime/disponibilite?periode=24h|7j|30j|90j— la disponibilité calculée en temps, pas en nombre de relevés : le temps d'indisponibilité vient des incidents, le temps de maintenance déclarée en est retiré. C'est ce qui rend le budget d'erreur lisible : « il te reste 12 min avant de manquer ton objectif du mois »./uptime/maintenance— les fenêtres déclarées à l'avance.
Les quotas
- Poste `sondes` : une unité par observation, prélevée avant de sonder. Un battement reçu en consomme une aussi — il écrit exactement ce qu'écrit une sonde. Un contrôle sauté faute de quota ne laisse pas de relevé : inventer un « inconnu » ferait baisser une disponibilité affichée pour une raison étrangère au site surveillé.
- Poste `controles` : le nombre de contrôles déclarés, tous types confondus. C'est le forfait qui décide, plus une constante.
- Poste `alertes` : les e-mails d'ouverture, de relance et de retour. Les canaux webhook et Slack ne le consomment pas — compter deux fois la même alerte ferait taire le courrier au milieu d'une panne.
intervalleMindu forfait borne la cadence la plus serrée.- Rétention : 30 jours de relevés, un an d'incidents et de maintenances.
Les pièges
- Une sonde isolée ne prouve rien. Le réglage
confirmations(2 par défaut, 1 à 5) dit combien d'observations concordantes il faut avant d'alerter. Entre-temps l'état est « suspect » : visible à l'écran, sans réveiller personne. Mettre 1 fait des fausses alertes, et au troisième plus personne ne lit les alertes. - « lent » n'alerte jamais. C'est un signal à lire, pas une urgence. Au-delà de trois secondes une page est lente ; pour en faire une panne, poser un
maxMs. - Chercher un texte dans une page qui redirige est un contresens : le corps d'un 302 est vide. Le contrôle le dit et demande l'adresse finale, au lieu d'accuser la page d'avoir perdu son contenu.
- Le type d'un contrôle ne se modifie pas. Changer un HTTP en battement garderait ses relevés, ses incidents et sa disponibilité, qui ne parlent plus de la même chose. En créer un autre.
- Un battement n'est jamais sondé. Il n'apparaît pas dans les contrôles dus, et son retour au vert n'est pas constaté par le worker : c'est l'appel du client qui referme l'incident.
- Le délai de grâce d'un battement n'est pas décoratif. Une sauvegarde prévue toutes les 24 h prend vingt minutes un mardi et quarante un vendredi. Sans grâce, le contrôle alerte au premier vendredi. Par défaut : le quart de l'intervalle, entre 1 et 60 minutes.
- Un contrôle DNS dont le résolveur ne répond pas est « lent », pas « en panne ». C'est notre résolveur qui a failli, pas le domaine du client. Le distinguer est tout le sujet : « aucun enregistrement » est une panne, « je n'ai pas pu demander » n'en est pas une.
- Un contrôle SSL passe en panne à sept jours de l'expiration, pas le jour J : un certificat qui expire dans la semaine est une panne programmée, et la traiter autrement ne la ferait pas renouveler. Entre 8 et 30 jours, il est « lent ».
- L'escalade est facultative et vaut null par défaut. Sans elle, la panne est dite à l'ouverture et au retour, jamais entre les deux — un e-mail par minute pendant six heures fait désactiver les alertes. Avec elle (5 minutes à 24 heures), la panne se redit à chaque échéance, et une seule fois par échéance.
- Une maintenance déclarée ne fait pas taire la confirmation. On continue de compter les échecs, pour que la panne soit déjà confirmée si elle survit à la fenêtre. Le temps de maintenance est relevé comme tel et ne compte ni en vert ni en rouge.
- Deux contrôles sur le même hôte et le même type doivent différer par le chemin, la cible ou le port. Deux ports TCP du même hôte coexistent ; deux fois le même, non.