Surveiller une tâche cron avec des heartbeats
Tâches cron qui échouent sans bruit : surveillez-les avec un heartbeat (ping curl, durée, code de sortie, délai de grâce, exemple Laravel).
- Publié le
- Mis à jour le
- Temps de lecture
- 8 min de lecture
Sur cette page
- Pourquoi les tâches cron échouent en silence
- Le principe du heartbeat
- Le ping le plus simple : une ligne de crontab
- && ou ; : un caractère qui change tout
- Mesurer la durée et le code de sortie
- Distinguer les exécutions concurrentes avec ?rid=
- Joindre la sortie de la tâche
- Régler le planning attendu
- Période simple ou expression cron
- Le fuseau horaire
- Le délai de grâce
- La durée maximale
- Avec le planificateur de Laravel
- Les pièges classiques
- Le PATH de cron
- Le caractère %
- Les exécutions qui se chevauchent
- Quelles tâches surveiller en priorité
- Mettre en place un premier heartbeat
Une sauvegarde qui ne tourne plus depuis trois semaines, des factures qui ne partent pas le 1er du mois : les pannes de tâches planifiées ont un point commun, personne ne les voit. Aucune page d'erreur, aucune sonde HTTP au rouge. On découvre le problème le jour où l'on a besoin du résultat.
Ce guide explique comment surveiller une tâche cron avec un heartbeat, du simple ping en fin de crontab jusqu'au réglage fin du planning. Les exemples utilisent LucidStack, mais la méthode vaut pour n'importe quel outil du même type.
Pourquoi les tâches cron échouent en silence
cron lance des commandes, rien de plus. Il ne vérifie pas qu'elles ont réussi et ne relance rien. Sa seule remontée d'erreur est un e-mail local envoyé à MAILTO, que personne ne lit. Ajoutez le classique > /dev/null 2>&1 en fin de ligne et toute trace disparaît.
Les causes sont variées :
- le serveur a été remplacé ou redéployé, et la crontab n'a pas suivi ;
- le démon cron est arrêté, ou le conteneur qui devait l'exécuter ne le lance pas ;
- le script plante sur une dépendance absente, un disque plein ou un identifiant expiré ;
- la tâche tourne beaucoup trop longtemps et chevauche l'exécution suivante.
Dans tous ces cas, le symptôme est le même : quelque chose qui aurait dû se produire ne s'est pas produit. Or une surveillance classique détecte la présence d'une erreur, pas l'absence d'un événement.
Le principe du heartbeat
Le heartbeat inverse la logique. Au lieu d'interroger la tâche, c'est la tâche qui se signale : à chaque exécution, elle envoie une requête HTTP à une URL qui lui est propre. Le service de surveillance connaît le planning attendu ; si le signal n'arrive pas à temps, ou s'il signale un échec, il vous alerte.
Aucun port à ouvrir ni agent à installer : la connexion est sortante, elle passe donc derrière un pare-feu ou un NAT, depuis n'importe quel langage.
Dans LucidStack, chaque heartbeat possède une URL de ping unique de la forme https://ping.lucidstack.dev/<jeton>, avec un jeton de 32 caractères. GET, POST et HEAD sont acceptés. Remplacez l'URL des exemples par la vôtre.
Le ping le plus simple : une ligne de crontab
# Sauvegarde tous les jours à 3 h, puis signal de vie si elle a réussi
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 https://ping.lucidstack.dev/k3x9q2m7v8w1z4r6t0y5u2p8n3b7c1d9 > /dev/null
Chaque option de curl a son rôle :
| Option | Rôle |
|---|---|
-f |
Considère une réponse HTTP d'erreur comme un échec, au lieu de l'ignorer |
-s |
Mode silencieux : pas de barre de progression dans les journaux |
-S |
Affiche tout de même les messages d'erreur malgré -s |
-m 10 |
Abandonne au bout de 10 secondes : un réseau lent ne bloque jamais la tâche |
--retry 3 |
Réessaie jusqu'à trois fois en cas d'erreur passagère |
La redirection > /dev/null jette la réponse du serveur mais conserve la sortie d'erreur : si le ping lui-même échoue, vous le verrez dans les journaux.
&& ou ; : un caractère qui change tout
Avec &&, curl ne s'exécute que si la sauvegarde se termine avec le code 0. En cas d'échec, pas de ping, et l'alerte part à l'expiration du délai de grâce. Avec ;, curl s'exécute dans tous les cas : le heartbeat vous dirait que tout va bien alors que la sauvegarde vient d'échouer.
Si vous tenez au point-virgule, transmettez le code de sortie de la commande :
0 3 * * * /usr/local/bin/backup.sh; curl -fsS -m 10 --retry 3 "https://ping.lucidstack.dev/k3x9q2m7v8w1z4r6t0y5u2p8n3b7c1d9?exit_code=$?" > /dev/null
Un code différent de 0 est enregistré comme un échec, et l'alerte part tout de suite au lieu d'attendre la fin du délai de grâce.
Mesurer la durée et le code de sortie
Pour connaître la durée de chaque exécution, envoyez deux signaux : /start au début, puis l'URL nue (ou /complete) à la fin. Le suffixe /fail clôt l'exécution en échec. Un petit script d'enrobage fait tout cela proprement :
#!/usr/bin/env bash
PING="https://ping.lucidstack.dev/k3x9q2m7v8w1z4r6t0y5u2p8n3b7c1d9"
RID=$(uuidgen)
LOG=$(mktemp)
curl -fsS -m 10 --retry 3 "$PING/start?rid=$RID" > /dev/null
/usr/local/bin/backup.sh > "$LOG" 2>&1
CODE=$?
# Les 10 derniers Ko de la sortie partent dans le corps de la requête
tail -c 10000 "$LOG" | curl -fsS -m 10 --retry 3 --data-binary @- \
"$PING?exit_code=$CODE&rid=$RID" > /dev/null
rm -f "$LOG"
exit $CODE
Distinguer les exécutions concurrentes avec ?rid=
Quand une même tâche tourne plusieurs fois en parallèle, sur plusieurs serveurs ou parce qu'une exécution déborde sur la suivante, le service ne peut pas deviner quel signal de fin correspond à quel /start. Le paramètre rid (identifiant d'exécution) les relie : un UUID suffit.
Joindre la sortie de la tâche
Le corps de la requête, jusqu'à 10 Ko, est conservé comme sortie de l'exécution pendant 90 jours. Le suffixe /log permet aussi d'attacher du texte sans clore l'exécution, par exemple une étape intermédiaire. Quand l'alerte arrive, vous avez déjà les dernières lignes du journal sous les yeux, sans vous connecter en SSH.
Régler le planning attendu
Période simple ou expression cron
Une période (toutes les heures, tous les jours) suffit quand l'heure exacte importe peu, par exemple pour un timer systemd. Pour une vraie tâche cron, reprenez plutôt son expression : 0 3 * * 1-5 ne tourne pas le week-end, et une période de 24 heures vous alerterait à tort chaque samedi. Notre outil d'expression cron affiche les prochaines exécutions dans le fuseau de votre choix.
Le fuseau horaire
cron utilise le fuseau du serveur, souvent UTC sur les machines cloud. Si la crontab dit 3 h en UTC et que le heartbeat attend 3 h à Paris, l'écart d'une ou deux heures produira des alertes injustifiées : choisissez le même fuseau que la machine. Méfiez-vous aussi des changements d'heure : selon l'implémentation de cron, une tâche placée entre 2 h et 3 h du matin peut être sautée, décalée ou exécutée deux fois ces jours-là. Évitez ce créneau, ou faites tourner le serveur en UTC.
Le délai de grâce
Le délai de grâce est la tolérance accordée après l'heure prévue avant de déclencher une alerte. Si vous ne pinguez qu'en fin de tâche, il doit couvrir la durée normale d'exécution : une sauvegarde lancée à 3 h qui dure 20 minutes pingue vers 3 h 20, un délai de 40 minutes laisse de la marge. Trop court, il produit des faux positifs ; trop long, il retarde l'alerte.
La durée maximale
Avec un ping /start, vous pouvez aussi fixer une durée maximale. Si l'exécution n'est pas terminée à temps, vous êtes alerté alors même que la tâche tourne encore. C'est ainsi qu'on repère une tâche bloquée sur un verrou ou un appel réseau sans délai d'expiration.
Avec le planificateur de Laravel
Laravel propose des méthodes de ping intégrées au planificateur. Elles s'appuient sur le code de sortie de la commande :
// routes/console.php
use Illuminate\Support\Facades\Schedule;
$ping = 'https://ping.lucidstack.dev/k3x9q2m7v8w1z4r6t0y5u2p8n3b7c1d9';
Schedule::command('backup:run')
->dailyAt('03:00')
->timezone('Europe/Paris')
->withoutOverlapping()
->pingBefore("{$ping}/start")
->pingOnSuccess($ping)
->pingOnFailure("{$ping}/fail");
Reste le maillon dont tout dépend : la ligne * * * * * php artisan schedule:run de la crontab. Si elle disparaît lors d'une migration de serveur, plus rien ne tourne. Un heartbeat dédié au planificateur lève le doute :
Schedule::call(fn () => true)
->everyFiveMinutes()
->thenPing('https://ping.lucidstack.dev/a7c2e9g4j1l6n8p3r5t0v2x7z9b4d6f1');
L'application fournit des extraits prêts à copier pour la crontab, Bash, PHP, Laravel, Node.js et Python.
Les pièges classiques
Le PATH de cron
cron démarre avec un environnement minimal : un PATH souvent réduit à /usr/bin:/bin, sans lire votre .bashrc ni votre .profile. Un script qui fonctionne à la main échoue alors avec un « command not found ». Utilisez des chemins absolus, ou déclarez le PATH en tête de crontab.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Le caractère %
Dans une crontab, % signifie un retour à la ligne. Une commande comme date +%F est donc coupée en deux. Échappez-le : date +\%F.
Les exécutions qui se chevauchent
Une tâche toutes les dix minutes qui en dure parfois quinze finira par tourner en double. flock garantit une seule exécution à la fois :
*/10 * * * * flock -n /var/lock/import.lock /usr/local/bin/import.sh && curl -fsS -m 10 --retry 3 https://ping.lucidstack.dev/k3x9q2m7v8w1z4r6t0y5u2p8n3b7c1d9 > /dev/null
Avec -n, si l'exécution précédente tient encore le verrou, flock abandonne avec une erreur : pas de ping, donc une alerte.
Quelles tâches surveiller en priorité
- Les sauvegardes : exports de base de données, copies hors site.
- Les imports et synchronisations : catalogue, stocks, CRM, flux partenaires.
- La facturation : génération des factures, relances, nouvelles tentatives de paiement.
- Les files d'attente : planifiez toutes les cinq minutes un petit job placé dans la file, qui pingue une fois traité. Si les workers sont arrêtés ou la file saturée, le ping manque.
- La maintenance : renouvellement des certificats, purge des journaux, génération du sitemap.
Pour la file d'attente, le job tient en quelques lignes :
// app/Jobs/QueueHeartbeat.php (extrait)
public function handle(): void
{
Http::timeout(10)->retry(3, 1000)->get('https://ping.lucidstack.dev/<jeton>');
}
// routes/console.php
Schedule::job(new QueueHeartbeat)->everyFiveMinutes();
Mettre en place un premier heartbeat
Commencez par la tâche dont la panne vous ferait le plus mal, en général la sauvegarde. Avec LucidStack, la surveillance des tâches cron se configure en quelques minutes : planning, délai de grâce, durée maximale, puis la ligne curl à coller. Les alertes de retard, d'échec ou de durée dépassée partent sur les canaux d'alerte de votre choix, avec la durée, le code de sortie et la sortie de la tâche. Le plan gratuit inclut 3 heartbeats, sans carte bancaire ; les volumes des autres plans sont détaillés sur la page tarifs.