Cron job’larınız sessizce çalışıyor, ama ne yapıyor? Hata mı veriyor, başarıyla mı tamamlanıyor, yoksa hiç çalışmıyor mu? Bu soruların cevabını bilmiyorsanız, otomasyonunuzun sizi ne zaman yarı yolda bırakacağını da bilemezsiniz. Cron çıktısını düzgün loglama ve bildirim sistemi kurmak, sysadmin hayatının en temel ama en çok ihmal edilen konularından biridir.
Cron’un Varsayılan Davranışını Anlamak
Cron, varsayılan olarak çalıştırdığı komutların stdout ve stderr çıktısını e-posta ile göndermaya çalışır. Sistemde bir mail transfer agent (MTA) kurulu ve yapılandırılmışsa, bu e-postalar root veya ilgili kullanıcıya iletilir. Ancak çoğu minimal sunucu kurulumunda MTA ya yoktur ya da düzgün yapılandırılmamıştır. Bu durumda çıktılar kaybolur gider.
Bir de şu senaryo var: MTA kurulu ama doğru yapılandırılmamış. Cron her çalıştığında e-posta göndermeye çalışır, başarısız olur, siz de sistemin /var/spool/mail/root dosyasının gigabyte’lara ulaştığını fark ettiğinizde çok geç olmuştur.
Şu komutu çalıştırarak sisteminizde ne kadar birikmiş cron maili olduğuna bir bakın:
ls -lh /var/spool/mail/root
wc -l /var/spool/mail/root
Eğer bu dosya birkaç MB’ı geçmişse, sessiz bir kaos içinde yaşıyorsunuz demektir.
Çıktıyı Tamamen Susturmak (Kötü Pratik)
Birçok sysadmin ve geliştirici, cron çıktısıyla uğraşmak yerine şu kısayolu tercih eder:
# Tüm çıktıyı /dev/null'a yönlendir - BUNU YAPMAYIN
0 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
Bu yaklaşım kısa vadede rahatlatıcı görünür ama uzun vadede felakete davet çıkarmaktır. Script sessizce başarısız olur, siz hiçbir şey fark etmezsiniz, bir gün yedeklerinizin 3 aydır alınmadığını keşfedersiniz.
Doğru yaklaşım: Çıktıyı susturmak yerine onu yakalamak, loglamak ve önemli durumlarda bildirim göndermek.
Temel Log Yönlendirme
En basit yöntem, cron çıktısını doğrudan bir log dosyasına yazmaktır:
# Sadece stdout'u logla
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log
# Hem stdout hem stderr'i logla
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# stdout ve stderr'i ayrı dosyalara yaz
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>> /var/log/backup_errors.log
Tarih damgası eklemek için ise şunu kullanabilirsiniz:
0 2 * * * echo "=== $(date '+%Y-%m-%d %H:%M:%S') ===" >> /var/log/backup.log && /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Bu yöntem işe yarar ama birkaç sorunu vardır: Log dosyaları sonsuz büyür, her çalışmanın nerede başlayıp bittiğini takip etmek zorlaşır ve hata durumunda bildirim alamazsınız.
Script İçinde Loglama Yapısı Kurmak
Gerçek dünyada, loglama mantığını crontab satırına değil, scriptin kendisine gömmek çok daha sürdürülebilirdir. İşte temel bir loglama fonksiyonu içeren script iskeleti:
#!/bin/bash
# Konfigürasyon
SCRIPT_NAME="backup"
LOG_DIR="/var/log/cron-jobs"
LOG_FILE="${LOG_DIR}/${SCRIPT_NAME}.log"
LOCK_FILE="/tmp/${SCRIPT_NAME}.lock"
EMAIL_TO="[email protected]"
MAX_LOG_SIZE=10485760 # 10MB
# Log dizini yoksa oluştur
mkdir -p "$LOG_DIR"
# Log fonksiyonu
log() {
local level="$1"
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] $*" | tee -a "$LOG_FILE"
}
# Başlangıç zamanını kaydet
START_TIME=$(date +%s)
log "INFO" "=============================="
log "INFO" "Job başladı: $SCRIPT_NAME"
log "INFO" "PID: $$"
log "INFO" "Kullanıcı: $(whoami)"
# Ana işlem
main() {
log "INFO" "Yedekleme başlıyor..."
# Buraya asıl iş mantığı gelir
rsync -av /data/ /backup/data/ >> "$LOG_FILE" 2>&1
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log "ERROR" "rsync başarısız oldu, exit code: $exit_code"
return $exit_code
fi
log "INFO" "Yedekleme başarıyla tamamlandı"
return 0
}
main
EXIT_CODE=$?
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
log "INFO" "Job tamamlandı. Süre: ${DURATION} saniye, Exit code: $EXIT_CODE"
log "INFO" "=============================="
exit $EXIT_CODE
Bu yapıyla crontab satırınız temiz kalır:
0 2 * * * /usr/local/bin/backup.sh
Log Rotation: Dosyaların Kontrolden Çıkmasını Önlemek
Log dosyaları büyür ve bir gün diski doldurur. Bunu önlemek için logrotate kullanmalısınız. /etc/logrotate.d/cron-jobs dosyasını oluşturun:
/var/log/cron-jobs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
dateext
dateformat -%Y%m%d
create 640 root adm
}
Bu konfigürasyon ile loglarınız:
- Her gün rotate edilir
- 30 günlük geçmişi tutar
- Gzip ile sıkıştırılır
- Boşsa işlem yapmaz
- Dosya adına tarih ekler
Logrotate konfigürasyonunu test etmek için:
logrotate -d /etc/logrotate.d/cron-jobs # Dry run
logrotate -f /etc/logrotate.d/cron-jobs # Zorla uygula
E-posta Bildirimi: Sadece Önemli Olaylarda
Her cron çalışmasında e-posta almak istemezsiniz, bu çok gürültüye yol açar. Ama hata durumunda mutlaka haberdar olmak istersiniz. İşte bunun için pratik bir yaklaşım:
#!/bin/bash
SCRIPT_NAME="db-backup"
LOG_FILE="/var/log/cron-jobs/${SCRIPT_NAME}.log"
EMAIL_TO="[email protected]"
EMAIL_FROM="cron@$(hostname -f)"
HOSTNAME=$(hostname -s)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$1] $2" | tee -a "$LOG_FILE"
}
send_alert() {
local subject="$1"
local body="$2"
# mail komutu ile gönder
if command -v mail &>/dev/null; then
echo "$body" | mail -s "[$HOSTNAME] $subject"
-a "From: $EMAIL_FROM"
"$EMAIL_TO"
log "INFO" "Uyarı e-postası gönderildi: $subject"
# sendmail alternatifi
elif command -v sendmail &>/dev/null; then
{
echo "From: $EMAIL_FROM"
echo "To: $EMAIL_TO"
echo "Subject: [$HOSTNAME] $subject"
echo ""
echo "$body"
} | sendmail -t
else
log "WARN" "Mail komutu bulunamadı, e-posta gönderilemedi"
fi
}
main() {
log "INFO" "DB yedekleme başlıyor"
# Yedekleme işlemi
if ! pg_dump mydb > /backup/mydb_$(date +%Y%m%d).sql 2>> "$LOG_FILE"; then
local error_msg
error_msg="PostgreSQL yedekleme başarısız oldu!n"
error_msg+="Sunucu: $HOSTNAMEn"
error_msg+="Zaman: $(date)n"
error_msg+="Log dosyası: $LOG_FILEnn"
error_msg+="Son log satırları:n"
error_msg+=$(tail -20 "$LOG_FILE")
send_alert "HATA: DB yedekleme başarısız" "$error_msg"
return 1
fi
log "INFO" "DB yedekleme başarıyla tamamlandı"
# Opsiyonel: Başarı bildirimi (haftalık özet mantığı için)
# send_alert "BILGI: DB yedekleme tamamlandı" "..."
return 0
}
main
exit $?
Wrapper Script Yaklaşımı: Tüm Job’lara Uygulanabilir Çözüm
Onlarca cron job’unuz varsa, her birinde loglama ve bildirim kodunu tekrarlamak istemezsiniz. Bunun için evrensel bir wrapper script yazabilirsiniz:
#!/bin/bash
# /usr/local/bin/cron-wrapper
# Kullanım: cron-wrapper [--alert-on-fail] [--alert-always] -- <komut> [argümanlar]
ALERT_ON_FAIL=false
ALERT_ALWAYS=false
LOG_DIR="/var/log/cron-jobs"
EMAIL_TO="${CRON_ALERT_EMAIL:-root}"
# Argümanları parse et
while [[ "$1" == --* ]]; do
case "$1" in
--alert-on-fail) ALERT_ON_FAIL=true ;;
--alert-always) ALERT_ALWAYS=true ;;
--email) EMAIL_TO="$2"; shift ;;
--) shift; break ;;
esac
shift
done
# Komut belirlenmemişse çık
if [ $# -eq 0 ]; then
echo "Hata: Çalıştırılacak komut belirtilmedi" >&2
exit 1
fi
# Job adını komuttan türet
JOB_NAME=$(basename "$1" | sed 's/[^a-zA-Z0-9_-]/_/g')
LOG_FILE="${LOG_DIR}/${JOB_NAME}_$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$LOG_DIR"
# Komutu çalıştır ve çıktıyı yakala
START_TIME=$(date +%s)
{
echo "=== Job Başladı ==="
echo "Komut: $*"
echo "Zaman: $(date)"
echo "Host: $(hostname)"
echo "==================="
} > "$LOG_FILE"
"$@" >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
{
echo "==================="
echo "Zaman: $(date)"
echo "Exit Code: $EXIT_CODE"
echo "Süre: ${DURATION}s"
echo "=== Job Bitti ==="
} >> "$LOG_FILE"
# Bildirim mantığı
SHOULD_NOTIFY=false
if $ALERT_ALWAYS; then
SHOULD_NOTIFY=true
elif $ALERT_ON_FAIL && [ $EXIT_CODE -ne 0 ]; then
SHOULD_NOTIFY=true
fi
if $SHOULD_NOTIFY; then
STATUS="BASARILI"
[ $EXIT_CODE -ne 0 ] && STATUS="BASARISIZ"
mail -s "[$(hostname)] Cron Job $STATUS: $JOB_NAME"
"$EMAIL_TO" < "$LOG_FILE"
fi
exit $EXIT_CODE
Bu wrapper’ı kullanarak crontab’ınızı şöyle düzenleyebilirsiniz:
# Sadece hata durumunda bildir
0 2 * * * /usr/local/bin/cron-wrapper --alert-on-fail -- /usr/local/bin/backup.sh
# Her durumda bildir
0 6 * * 1 /usr/local/bin/cron-wrapper --alert-always -- /usr/local/bin/weekly-report.sh
# Özel e-posta adresiyle
0 3 * * * /usr/local/bin/cron-wrapper --alert-on-fail --email [email protected] -- /usr/local/bin/deploy-check.sh
Systemd-email: Modern Yaklaşım
Eğer sistematemleri systemd ile yönetiyorsanız, OnCalendar ile cron yerine systemd timer kullanmayı düşünebilirsiniz. Ancak klasik cron kullanmaya devam ediyorsanız, systemd’nin e-posta sistemini taklit eden bir yapı kurabilirsiniz.
/etc/systemd/system/[email protected] dosyası oluşturun:
[Unit]
Description=Job status email for %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/job-status-email.sh %i
User=root
Ve ilgili scripti hazırlayın:
#!/bin/bash
# /usr/local/bin/job-status-email.sh
JOB="$1"
LOG_DIR="/var/log/cron-jobs"
EMAIL_TO="[email protected]"
# En son log dosyasını bul
LATEST_LOG=$(ls -t "${LOG_DIR}/${JOB}"_*.log 2>/dev/null | head -1)
if [ -z "$LATEST_LOG" ]; then
echo "Log dosyası bulunamadı: $JOB" |
mail -s "Cron Log Eksik: $JOB" "$EMAIL_TO"
exit 1
fi
# Exit code'u log dosyasından çıkar
EXIT_CODE=$(grep "Exit Code:" "$LATEST_LOG" | tail -1 | awk '{print $3}')
if [ "$EXIT_CODE" != "0" ]; then
mail -s "[HATA] Cron Job Başarısız: $JOB" "$EMAIL_TO" < "$LATEST_LOG"
fi
Gerçek Dünya Senaryosu: Yedekleme Monitoring Sistemi
Birkaç yıl önce bir müşterinin sunucusunu devraldım. Sunucuda 15 farklı cron job vardı, hepsinin çıktısı /dev/null‘a gidiyordu. Yedekleme scriptleri “çalışıyor” gibiydi ama aslında 4 aydır sessizce başarısız oluyordu. Bir ransomware saldırısında geri dönecek yedek yoktu.
Bu deneyimden sonra kurduğum sistemin özeti:
#!/bin/bash
# /usr/local/bin/backup-monitor.sh
# Her sabah çalışarak önceki gecenin yedeklerini kontrol eder
LOG_FILE="/var/log/cron-jobs/backup-monitor.log"
BACKUP_DIR="/backup"
ALERT_EMAIL="[email protected]"
MAX_AGE_HOURS=25 # 25 saatten eski yedek varsa uyar
ALERT_BODY=""
HAS_ERRORS=false
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
check_backup() {
local name="$1"
local pattern="$2"
local dir="${3:-$BACKUP_DIR}"
# En son yedek dosyasını bul
local latest_file
latest_file=$(find "$dir" -name "$pattern" -type f
-printf '%T@ %pn' 2>/dev/null |
sort -n | tail -1 | awk '{print $2}')
if [ -z "$latest_file" ]; then
log "ERROR: $name - Yedek dosyası bulunamadı"
ALERT_BODY+="HATA: $name yedek dosyası yokn"
HAS_ERRORS=true
return 1
fi
# Dosyanın yaşını kontrol et
local file_age_hours
file_age_hours=$(( ($(date +%s) - $(stat -c %Y "$latest_file")) / 3600 ))
if [ "$file_age_hours" -gt "$MAX_AGE_HOURS" ]; then
log "WARN: $name - Son yedek ${file_age_hours} saat önce"
ALERT_BODY+="UYARI: $name son yedek ${file_age_hours} saat önce alınmışn"
HAS_ERRORS=true
else
local size
size=$(du -sh "$latest_file" | cut -f1)
log "OK: $name - Son yedek ${file_age_hours} saat önce ($size)"
fi
}
# Her yedek kaynağını kontrol et
check_backup "PostgreSQL Ana DB" "pgdump_main_*.sql.gz"
check_backup "MySQL CRM DB" "mysql_crm_*.sql.gz"
check_backup "Dosya Sistemi" "files_*.tar.gz" "/backup/files"
check_backup "Konfigürasyon" "config_*.tar.gz" "/backup/configs"
# Sorun varsa e-posta gönder
if $HAS_ERRORS; then
{
echo "Yedekleme Kontrol Raporu - $(date)"
echo "Sunucu: $(hostname)"
echo "================================"
echo -e "$ALERT_BODY"
echo "================================"
echo "Detaylı log: $LOG_FILE"
} | mail -s "[$(hostname)] Yedekleme Sorunu Tespit Edildi" "$ALERT_EMAIL"
log "Uyarı e-postası gönderildi"
fi
Bu script crontab’a şöyle eklenir:
# Her sabah 07:00'de yedek kontrolü yap
0 7 * * * /usr/local/bin/backup-monitor.sh
Cron Log’larını Merkezi Bir Yerden Takip Etmek
Birden fazla sunucu yönetiyorsanız, her birinde ayrı ayrı log kontrol etmek can sıkıcı olur. Basit bir çözüm olarak tüm cron loglarını syslog’a yönlendirebilirsiniz:
# /etc/rsyslog.d/cron-to-file.conf
# Cron loglarını ayrı dosyaya yaz
cron.* /var/log/cron-all.log
# Varsa uzak syslog sunucusuna gönder
cron.* @@logserver.example.com:514
Script içinden syslog’a yazmak için logger komutunu kullanabilirsiniz:
#!/bin/bash
SCRIPT_NAME="my-cron-job"
log() {
local level="$1"
shift
# Hem dosyaya hem syslog'a yaz
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "/var/log/cron-jobs/${SCRIPT_NAME}.log"
logger -t "cron-${SCRIPT_NAME}" -p "cron.${level,,}" "$*"
}
log "INFO" "Job başladı"
# ... işlemler ...
log "INFO" "Job tamamlandı"
MAILTO Değişkeni ile Crontab E-posta Kontrolü
Crontab’ın başına MAILTO değişkeni ekleyerek tüm cron e-postalarını yönlendirebilirsiniz:
# Tüm cron çıktılarını bu adrese gönder
MAILTO="[email protected]"
# E-posta gönderimi tamamen kapat
# MAILTO=""
# Her iş için farklı adres crontab içinde override edilebilir
0 2 * * * /usr/local/bin/backup.sh
MAILTO="[email protected]"
0 3 * * * /usr/local/bin/db-maintenance.sh
MAILTO="[email protected]"
0 4 * * * /usr/local/bin/system-check.sh
Lock File ile Mükerrer Çalışma Tespiti
Uzun süren bir cron job’un bir sonraki tetiklenmeden önce bitmemesi durumunda iki instance aynı anda çalışmaya başlar. Bu, loglama açısından da kafa karıştırıcı olur. Lock file mekanizması hem bunu önler hem de loglarda uyarı bırakır:
#!/bin/bash
LOCK_FILE="/tmp/my-job.lock"
LOG_FILE="/var/log/cron-jobs/my-job.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; }
# Lock kontrolü
if [ -f "$LOCK_FILE" ]; then
OLD_PID=$(cat "$LOCK_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
log "WARN: Job zaten çalışıyor (PID: $OLD_PID). Atlanıyor."
echo "UYARI: Çakışan job tespit edildi" |
mail -s "[$(hostname)] Cron Job Çakışması" [email protected]
exit 0
else
log "INFO: Eski lock dosyası temizleniyor (PID: $OLD_PID artık yok)"
rm -f "$LOCK_FILE"
fi
fi
# Lock oluştur
echo $$ > "$LOCK_FILE"
trap "rm -f $LOCK_FILE; log 'INFO: Lock dosyası temizlendi'" EXIT
log "INFO" "Job başladı (PID: $$)"
# ... asıl iş ...
log "INFO" "Job tamamlandı"
Sonuç
Cron job’larınızı “kurdum çalışıyor” modundan çıkarıp gerçek anlamda izlenebilir bir sisteme taşımak için yapmanız gerekenler aslında karmaşık değil. Birkaç temel prensibe bağlı kalın:
- Hiçbir zaman
> /dev/null 2>&1ile çıktıyı tamamen kapatmayın, bir şeyler ters gittiğinde kör olursunuz - Her script’te en azından tarih damgalı loglama yapın
- Hata durumlarını yakalayin ve e-posta ya da başka bir bildirim mekanizmasıyla haberdar olun
- Log dosyalarını logrotate ile yönetin, aksi halde diskler dolar
- Lock file kullanarak mükerrer çalışmaları önleyin ve loglayın
- Birden fazla sunucu varsa merkezi log çözümü düşünün
Bu adımları uyguladıktan sonra bir gün “yedeklerim nerede?” sorusunu sorduğunuzda, cevabı hemen bulabilirsiniz. Ve daha da önemlisi, o soruyu sormadan önce sistemin size söylemiş olduğunu fark edersiniz.