Перейти к содержимому

Уведомления о неудачных cron-задачах

По умолчанию cron шлёт ошибки в локальную почту root, которую никто не читает. Заменим этот канал на Notifly: каждая упавшая задача → push на телефон с последними строками вывода.

Используйте уже готовый notifly-wrap из рецепта про бэкапы. В cron задачи будут выглядеть так:

*/10 * * * * www-data /usr/local/bin/notifly-wrap "Sync ratings" -- /usr/local/bin/sync-ratings.sh
0 * * * * root /usr/local/bin/notifly-wrap "Yum updates" -- yum -y update
30 3 * * * root /usr/local/bin/notifly-wrap "Logrotate" -- logrotate -f /etc/logrotate.conf

Зелёные «✅» можно подавить, изменив обёртку — отправлять только при rc != 0. Минимальная «только-падения» версия:

#!/usr/bin/env bash
set -eu
set -a; source /etc/notifly.env; set +a
NAME="${1:?}"; shift; [ "${1:-}" = "--" ] && shift
LOG=$(mktemp)
if ! "$@" >"$LOG" 2>&1; then
RC=$?
/usr/local/bin/notifly-send \
"❌ cron: $NAME (rc=$RC) — $(hostname -s)" \
"$(tail -c 1500 "$LOG")" 8
rm -f "$LOG"; exit "$RC"
fi
rm -f "$LOG"

Если не хочется править все cron-задачи, можно перехватить почту, которую cron шлёт root, и отправить её в Notifly.

В /etc/aliases (или /etc/postfix/aliases) добавьте:

root: |/usr/local/bin/notifly-mailpipe

/usr/local/bin/notifly-mailpipe:

#!/usr/bin/env bash
set -eu
set -a; source /etc/notifly.env; set +a
BODY=$(cat | head -c 2000)
[ -z "$BODY" ] && exit 0
/usr/local/bin/notifly-send \
"📬 cron-вывод на $(hostname -s)" \
"$BODY" 6
Окно терминала
sudo chmod +x /usr/local/bin/notifly-mailpipe
sudo newaliases

Теперь любой cron-job, который что-то выводит в stdout/stderr, превращается в push-уведомление. Это «грязный, но универсальный» способ — годится, чтобы быстро накрыть унаследованный сервер.

Иногда сам cron-демон может не запуститься (например, после редактирования crontab с ошибкой). Заведите простой «канарейку», которая шлёт сообщение, если не было heartbeat:

*/5 * * * * root touch /tmp/cron-alive

И отдельный systemd-таймер:

/etc/systemd/system/notifly-cron-canary.timer
[Unit]
Description=Cron canary check
[Timer]
OnCalendar=*:0/30
Persistent=true
[Install]
WantedBy=timers.target
/etc/systemd/system/notifly-cron-canary.service
[Service]
Type=oneshot
EnvironmentFile=/etc/notifly.env
ExecStart=/bin/bash -c '\
AGE=$(( $(date +%%s) - $(stat -c %%Y /tmp/cron-alive 2>/dev/null || echo 0) )); \
if [ "$AGE" -gt 900 ]; then \
/usr/local/bin/notifly-send "⚠️ Cron мёртв на $(hostname -s)" \
"Heartbeat-файл старше 15 минут (возраст ${AGE}s)." 10; \
fi'
Окно терминала
sudo systemctl daemon-reload
sudo systemctl enable --now notifly-cron-canary.timer

Windows-аналог cron — Task Scheduler. Он самостоятельно отслеживает exit-code, поэтому вместо «обёрток» проще всего подписаться на события «задача завершилась с ошибкой» в журнале Microsoft-Windows-TaskScheduler/Operational (Event ID 203).

C:\scripts\Notifly-TaskFailed.ps1
param([string]$TaskName, [string]$ResultCode)
. C:\scripts\Notifly.ps1
Send-Notifly `
-Title "❌ Task Scheduler: $TaskName$env:COMPUTERNAME" `
-Message "Код возврата: $ResultCode" `
-Priority 8

Подписка на все фейлы Task Scheduler:

Окно терминала
$xml = @"
<QueryList>
<Query Id="0" Path="Microsoft-Windows-TaskScheduler/Operational">
<Select Path="Microsoft-Windows-TaskScheduler/Operational">
*[System[(EventID=203 or EventID=323)]]
</Select>
</Query>
</QueryList>
"@
$Trigger = New-ScheduledTaskTrigger -AtStartup
$Trigger.Subscription = $xml
$Action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-TaskFailed.ps1 -TaskName 'Unknown' -ResultCode 'see Event Log'"
Register-ScheduledTask -TaskName "Notifly Watch Failed Tasks" -Trigger $Trigger -Action $Action `
-Principal (New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest)

Способ 2: персональная обёртка для одной задачи

Заголовок раздела «Способ 2: персональная обёртка для одной задачи»

Аналог notifly-wrap — запускает команду, ловит exit-code, при ошибке шлёт хвост лога:

C:\scripts\Notifly-Wrap.ps1
param([Parameter(Mandatory)] [string]$Name,
[Parameter(Mandatory, ValueFromRemainingArguments)] [string[]]$Cmd)
. C:\scripts\Notifly.ps1
$log = New-TemporaryFile
$start = Get-Date
& $Cmd[0] $Cmd[1..($Cmd.Count-1)] *> $log
$rc = $LASTEXITCODE
$dur = ((Get-Date) - $start).TotalSeconds
if ($rc -ne 0) {
$tail = Get-Content $log -Tail 30 | Out-String
Send-Notifly -Title "$Name (rc=$rc) — $env:COMPUTERNAME" `
-Message "Длительность: $([int]$dur)s`n`n$tail" `
-Priority 8
}
Remove-Item $log -Force
exit $rc

Использование в задаче: в поле «Action» укажите powershell.exe -NoProfile -File C:\scripts\Notifly-Wrap.ps1 -Name "Ночной бэкап" robocopy ....

  • Никаких «потерянных» ошибок в локальной почте. Каждая упавшая задача = push-уведомление.
  • Контекст в кармане — последние 1500 символов stderr-а, обычно этого достаточно для диагностики.
  • Канарейка ловит худший случай — когда сам cron не работает.
  • Отдельные каналы Notifly для разных типов задач (ETL, бэкап, мониторинг) — чтобы можно было фильтровать в админке и в клиенте.
  • Хранить «время последнего успеха» в Prometheus textfile collector и алертить, если он не обновляется N часов.