Cron Görevlerini İzleme ve Hata Tespiti

Cron job’ları sistemin sessiz işçileridir. Gece yarısı yedek alırlar, log dosyalarını temizlerler, raporları oluştururlar ve siz uyurken onlarca görevi hallederler. Peki ya bir şeyler ters gittiğinde? Çoğu sysadmin, bir cron job’ının haftalardır çalışmadığını ancak kritik bir sorun ortaya çıkınca fark eder. Bu yazıda, cron görevlerini proaktif olarak nasıl izleyeceğinizi, hataları nasıl tespit edeceğinizi ve kendinizi bu “sessiz felaket” senaryolarından nasıl koruyacağınızı ele alacağız.

Cron’un Sessiz Başarısızlık Sorunu

Cron’un en büyük problemi, varsayılan davranışının son derece “sessiz” olmasıdır. Bir görev başarısız olursa, sistem size bağırmaz. Belki bir e-posta gönderir, belki görmezden gelir. Çoğu ortamda e-posta yapılandırması düzgün çalışmadığı için bu bildirimler zaten ulaşmaz.

Tipik bir senaryo düşünün: Her gece çalışan veritabanı yedekleme scripti, disk dolduğu için 3 hafta önce sessizce başarısız olmaya başlamıştır. Siz bunu ancak veri kaybı yaşandığında öğrenirsiniz. Bu tam anlamıyla sysadmin kabusu.

Çözüm, cron görevlerinizi izlemek için savunmacı bir yaklaşım benimsemektir.

Temel Log Kaynaklarını Tanımak

Cron loglarının nerede olduğunu bilmek, sorun tespitinin ilk adımıdır.

# Debian/Ubuntu sistemlerde
tail -f /var/log/syslog | grep CRON

# RHEL/CentOS/Fedora sistemlerde
tail -f /var/log/cron

# Systemd tabanlı sistemlerde
journalctl -u cron -f
journalctl -u crond -f

# Son 1 saatteki cron aktivitesi
journalctl -u cron --since "1 hour ago"

Burada dikkat etmeniz gereken bir nokta var: Cron log satırları görevin başladığını söyler, başarıyla tamamlandığını değil. Yani log’da görevi görüyorsunuz diye her şeyin yolunda olduğunu düşünmeyin.

# Cron logunu daha anlamlı hale getirmek için
grep "CRON|cron" /var/log/syslog | grep -E "(error|failed|command not found)" 

Syslog’da Cron Seviyesini Artırmak

Daha ayrıntılı log almak istiyorsanız, cron daemon’ının log seviyesini artırabilirsiniz.

# /etc/rsyslog.conf veya /etc/rsyslog.d/50-default.conf dosyasına ekleyin
cron.*    /var/log/cron.log

# rsyslog'u yeniden başlatın
sudo systemctl restart rsyslog

Her Cron Job İçin Çıktı Yönetimi

Cron job’larınızın çıktısını düzgün yönetmek, hata tespitinin temelidir. Standart uygulama, stdout ve stderr’i ayrı dosyalara yönlendirmektir.

# Kötü yöntem - her şeyi /dev/null'a gönder, hiçbir şey öğrenemezsin
0 2 * * * /opt/scripts/backup.sh > /dev/null 2>&1

# İyi yöntem - her şeyi log dosyasına yaz
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# Daha iyi yöntem - stdout ve stderr'i ayır
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup_out.log 2>> /var/log/backup_err.log

# En iyi yöntem - timestamp ile birlikte logla
0 2 * * * echo "[$(date '+%Y-%m-%d %H:%M:%S')] Başlıyor" >> /var/log/backup.log && /opt/scripts/backup.sh >> /var/log/backup.log 2>&1 && echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tamamlandı" >> /var/log/backup.log

Script İçinde Loglama

Uzun vadeli çözüm için cron job’larınızı çalıştıran scriptlerin içine loglama mekanizması entegre edin.

#!/bin/bash
# /opt/scripts/backup.sh

LOG_FILE="/var/log/myapp/backup.log"
SCRIPT_NAME=$(basename "$0")
START_TIME=$(date '+%Y-%m-%d %H:%M:%S')

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$SCRIPT_NAME] $1" | tee -a "$LOG_FILE"
}

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$SCRIPT_NAME] [ERROR] $1" | tee -a "$LOG_FILE" >&2
}

# Başlangıç kaydı
log "Yedekleme başlatıldı"

# Disk alanı kontrolü
AVAILABLE=$(df /backup | awk 'NR==2 {print $4}')
if [ "$AVAILABLE" -lt 5242880 ]; then  # 5GB'dan az
    log_error "Yetersiz disk alanı: ${AVAILABLE}KB mevcut"
    exit 1
fi

# Yedekleme işlemi
if pg_dump mydb > /backup/mydb_$(date +%Y%m%d).sql; then
    log "Veritabanı yedeği başarıyla alındı"
else
    log_error "Veritabanı yedeği başarısız! Exit code: $?"
    exit 1
fi

log "Yedekleme tamamlandı - Süre: $((SECONDS)) saniye"

Exit Code Takibi: Gerçek Başarı Kriteri

Bir cron job’ının başarılı olup olmadığını anlamanın en güvenilir yolu, exit code’unu takip etmektir.

#!/bin/bash
# /opt/scripts/cron_wrapper.sh - Evrensel cron sarmalayıcı

COMMAND="$@"
LOG_FILE="/var/log/cron_wrapper.log"
SLACK_WEBHOOK="https://hooks.slack.com/services/XXXXX"

run_job() {
    local start_time=$(date +%s)
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    echo "[$timestamp] Başlatıldı: $COMMAND" >> "$LOG_FILE"
    
    # Komutu çalıştır ve çıktıyı yakala
    output=$($COMMAND 2>&1)
    exit_code=$?
    
    local end_time=$(date +%s)
    local duration=$((end_time - start_time))
    
    if [ $exit_code -ne 0 ]; then
        echo "[$timestamp] HATA (exit: $exit_code, süre: ${duration}s): $COMMAND" >> "$LOG_FILE"
        echo "Çıktı: $output" >> "$LOG_FILE"
        
        # Slack bildirimi gönder
        curl -s -X POST "$SLACK_WEBHOOK" 
            -H 'Content-type: application/json' 
            -d "{"text":"Cron Hatası!nKomut: $COMMANDnExit Code: $exit_codenSunucu: $(hostname)nÇıktı: $output"}"
    else
        echo "[$timestamp] BAŞARILI (süre: ${duration}s): $COMMAND" >> "$LOG_FILE"
    fi
    
    return $exit_code
}

run_job

Bu wrapper’ı crontab’ınızda şu şekilde kullanırsınız:

# Normal cron girişi
0 2 * * * /opt/scripts/backup.sh

# Wrapper ile
0 2 * * * /opt/scripts/cron_wrapper.sh /opt/scripts/backup.sh

Deadman’s Switch Yaklaşımı: Healthcheck Entegrasyonu

Cron izlemenin en profesyonel yöntemi, healthcheck servisleri kullanmaktır. Bu servislerin mantığı basittir: Cron job başarıyla tamamlandığında bir URL’e ping atar. Eğer belirli süre içinde ping gelmezse, servis size alarm verir.

Ücretsiz ve yaygın kullanılan seçenekler şunlardır:

  • Healthchecks.io – Açık kaynak, self-host edebilirsiniz
  • Cronitor – Ücretsiz tier mevcut
  • Dead Man’s Snitch – Basit ve etkili
# Healthchecks.io entegrasyonu
# Başarılı tamamlanma ping'i
0 2 * * * /opt/scripts/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://hc-ping.com/YOUR-UUID

# Başlangıç ve bitiş ping'i (daha ayrıntılı izleme)
0 2 * * * curl -fsS -m 10 https://hc-ping.com/YOUR-UUID/start && /opt/scripts/backup.sh && curl -fsS -m 10 https://hc-ping.com/YOUR-UUID

# Hata durumunda fail ping'i
0 2 * * * /opt/scripts/backup.sh; [ $? -ne 0 ] && curl -fsS -m 10 https://hc-ping.com/YOUR-UUID/fail || curl -fsS -m 10 https://hc-ping.com/YOUR-UUID

Self-Hosted Healthcheck ile İzleme

Kendi sunucunuzda basit bir healthcheck mekanizması kurabilirsiniz.

#!/bin/bash
# /opt/scripts/job_heartbeat.sh
# Her cron job çalıştığında bu scripti çağırın

JOB_NAME="$1"
STATUS="$2"  # success veya fail
HEARTBEAT_DIR="/var/lib/cron_heartbeats"

mkdir -p "$HEARTBEAT_DIR"

# Heartbeat dosyasını güncelle
echo "$(date '+%Y-%m-%d %H:%M:%S') $STATUS" > "${HEARTBEAT_DIR}/${JOB_NAME}.heartbeat"

# Monitoring scripti bu dosyaları okur
#!/bin/bash
# /opt/scripts/check_cron_health.sh
# Bu scripti de bir cron ile her 30 dakikada çalıştırın

HEARTBEAT_DIR="/var/lib/cron_heartbeats"
ALERT_EMAIL="[email protected]"
MAX_AGE_HOURS=25  # 24 saatlik job için 25 saat tolerans

check_job() {
    local job_name="$1"
    local max_age_hours="$2"
    local heartbeat_file="${HEARTBEAT_DIR}/${job_name}.heartbeat"
    
    if [ ! -f "$heartbeat_file" ]; then
        echo "UYARI: $job_name için heartbeat dosyası bulunamadı!"
        return 1
    fi
    
    local last_run=$(stat -c %Y "$heartbeat_file")
    local now=$(date +%s)
    local age_hours=$(( (now - last_run) / 3600 ))
    local last_status=$(tail -1 "$heartbeat_file" | awk '{print $3}')
    
    if [ "$age_hours" -gt "$max_age_hours" ]; then
        echo "HATA: $job_name son ${age_hours} saattir çalışmadı!"
        mail -s "Cron Alert: $job_name çalışmıyor - $(hostname)" "$ALERT_EMAIL" <<< "$job_name son ${age_hours} saattir çalışmadı!"
        return 1
    fi
    
    if [ "$last_status" = "fail" ]; then
        echo "HATA: $job_name son çalışmasında başarısız oldu!"
        return 1
    fi
    
    echo "OK: $job_name (${age_hours} saat önce, durum: $last_status)"
    return 0
}

# Her job için kontrol et
check_job "nightly_backup" 25
check_job "log_cleanup" 25
check_job "ssl_cert_check" 25

Cron’da Yaygın Sorunları Tespit Etme

Ortam Değişkeni Sorunları

Cron’da en sık karşılaşılan sorunlardan biri, cron’un minimal bir ortam ile çalışmasıdır. PATH, LD_LIBRARY_PATH gibi değişkenler eksik olabilir.

# Cron ortamını görmek için
* * * * * env > /tmp/cron_env.txt

# Crontab'ın başına ortam değişkenlerini ekle
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=""

# Scriptlerin başında tam yol kullanın
#!/bin/bash
export PATH=/usr/local/bin:/usr/bin:/bin
source /home/deploy/.bashrc

Kilitleme ve Çakışan İşler

Uzun süren bir cron job tamamlanmadan yeni bir instance başlarsa felaket olabilir.

#!/bin/bash
# Flock ile kilit mekanizması
LOCK_FILE="/var/lock/backup_job.lock"

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
    echo "$(date): Job zaten çalışıyor, atlanıyor" >> /var/log/backup.log
    exit 1
fi

# Buradan sonra asıl iş

echo "$(date): Yedekleme başlıyor" >> /var/log/backup.log
# ... yedekleme kodu ...
echo "$(date): Yedekleme tamamlandı" >> /var/log/backup.log

# Lock otomatik olarak serbest bırakılır

Zaman Dilimi Tuzakları

Özellikle cloud ortamlarında sunucu zaman dilimi ile beklentileriniz uyuşmayabilir.

# Sunucunun zaman dilimini kontrol et
timedatectl status
date

# Crontab'da zaman dilimini açıkça belirt
CRON_TZ=Europe/Istanbul
0 2 * * * /opt/scripts/backup.sh

# Veya sistemin zaman dilimini değiştir
sudo timedatectl set-timezone Europe/Istanbul

Log Rotasyonu ve Temizliği

Cron log dosyaları hızla büyüyebilir. Logrotate ile bu sorunu çözün.

# /etc/logrotate.d/cron-jobs dosyası oluşturun
/var/log/backup.log
/var/log/cron_wrapper.log
/var/log/myapp/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 root adm
    sharedscripts
    postrotate
        # Gerekirse servisleri bilgilendir
        /usr/bin/killall -HUP rsyslogd 2>/dev/null || true
    endscript
}

Merkezi İzleme: Birden Fazla Sunucuyu Takip Etme

Onlarca sunucu yönetiyorsanız, cron izlemeyi merkezi hale getirmeniz gerekir.

#!/bin/bash
# /opt/scripts/report_cron_status.sh
# Her sunucuda çalışır, merkezi log sunucusuna gönderir

CENTRAL_LOG_SERVER="logserver.sirket.com"
CENTRAL_LOG_PORT="514"
HOSTNAME=$(hostname -f)

report_to_central() {
    local job_name="$1"
    local status="$2"
    local duration="$3"
    local message="cron_monitor host=$HOSTNAME job=$job_name status=$status duration=${duration}s timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    
    # Syslog formatında gönder
    logger -n "$CENTRAL_LOG_SERVER" -P "$CENTRAL_LOG_PORT" -t "cron_monitor" "$message"
    
    # Veya HTTP endpoint'e gönder
    curl -s -X POST "http://${CENTRAL_LOG_SERVER}:8080/cron-events" 
        -H "Content-Type: application/json" 
        -d "{"host":"$HOSTNAME","job":"$job_name","status":"$status","duration":$duration}"
}

# Kullanım
START=$(date +%s)
/opt/scripts/backup.sh
STATUS=$?
DURATION=$(($(date +%s) - START))

if [ $STATUS -eq 0 ]; then
    report_to_central "nightly_backup" "success" "$DURATION"
else
    report_to_central "nightly_backup" "failure" "$DURATION"
fi

Cron Tarihçesini Analiz Etme

Mevcut loglardan cron görevlerinizin geçmişini analiz etmek için bazı kullanışlı one-liner’lar:

# Son 7 günde kaç kez çalıştı
grep "backup.sh" /var/log/syslog | grep "$(date -d '7 days ago' +%b)" | wc -l

# Başarısız çalışmaları bul
grep -E "[ERROR]|[FAIL]|exit code [^0]" /var/log/backup.log

# Ortalama çalışma süresini hesapla
grep "Tamamlandı - Süre:" /var/log/backup.log | 
    awk '{print $NF}' | 
    sed 's/saniye//' | 
    awk '{sum+=$1; count++} END {print "Ortalama süre:", sum/count, "saniye"}'

# Son 30 günün başarı oranı
total=$(grep -c "Yedekleme başlatıldı" /var/log/backup.log)
success=$(grep -c "Yedekleme tamamlandı" /var/log/backup.log)
echo "Başarı oranı: $((success * 100 / total))% ($success/$total)"

Gerçek Dünya Senaryosu: Kapsamlı Cron İzleme Sistemi

Tüm bu parçaları bir araya getiren, üretim ortamına uygun bir kurulum yapalım.

#!/bin/bash
# /opt/scripts/monitored_cron.sh
# Kullanım: monitored_cron.sh "job_adı" "max_sure_dakika" "komut"

JOB_NAME="${1:-unknown}"
MAX_DURATION_MIN="${2:-60}"
COMMAND="${@:3}"

LOG_DIR="/var/log/cron_monitor"
LOG_FILE="${LOG_DIR}/${JOB_NAME}.log"
STATE_DIR="/var/lib/cron_monitor"
STATE_FILE="${STATE_DIR}/${JOB_NAME}.state"
ALERT_EMAIL="${CRON_ALERT_EMAIL:[email protected]}"
LOCK_FILE="/var/lock/cron_${JOB_NAME}.lock"

mkdir -p "$LOG_DIR" "$STATE_DIR"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; }

# Kilit kontrolü
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
    log "SKIP: $JOB_NAME zaten çalışıyor, bu instance atlanıyor"
    exit 0
fi

# Maksimum süre kontrolü için arkaplan process
(
    sleep $((MAX_DURATION_MIN * 60))
    if kill -0 $$ 2>/dev/null; then
        log "TIMEOUT: $JOB_NAME ${MAX_DURATION_MIN} dakika sınırını aştı!"
        mail -s "Cron Timeout: $JOB_NAME - $(hostname)" "$ALERT_EMAIL" 
            <<< "$JOB_NAME görevi ${MAX_DURATION_MIN} dakika sınırını aştı!"
        kill $$
    fi
) &
TIMEOUT_PID=$!

START_TIME=$(date +%s)
log "START: $JOB_NAME başlatıldı - Komut: $COMMAND"

# Durumu "running" olarak kaydet
echo "running $(date '+%Y-%m-%d %H:%M:%S')" > "$STATE_FILE"

# Komutu çalıştır
eval "$COMMAND" >> "$LOG_FILE" 2>&1
EXIT_CODE=$?

DURATION=$(($(date +%s) - START_TIME))
kill $TIMEOUT_PID 2>/dev/null
wait $TIMEOUT_PID 2>/dev/null

if [ $EXIT_CODE -eq 0 ]; then
    log "SUCCESS: $JOB_NAME tamamlandı (${DURATION}s)"
    echo "success $(date '+%Y-%m-%d %H:%M:%S') ${DURATION}s" > "$STATE_FILE"
    # Healthcheck ping
    [ -n "$HEALTHCHECK_URL" ] && curl -fsS -m 10 "$HEALTHCHECK_URL" -o /dev/null
else
    log "FAILURE: $JOB_NAME başarısız (exit: $EXIT_CODE, süre: ${DURATION}s)"
    echo "failure $(date '+%Y-%m-%d %H:%M:%S') ${DURATION}s exit:${EXIT_CODE}" > "$STATE_FILE"
    
    # Hata bildirimi
    tail -50 "$LOG_FILE" | mail -s "Cron Failure: $JOB_NAME - $(hostname)" "$ALERT_EMAIL"
    [ -n "$HEALTHCHECK_URL" ] && curl -fsS -m 10 "${HEALTHCHECK_URL}/fail" -o /dev/null
fi

exit $EXIT_CODE

Bu scripti crontab’ınıza şöyle entegre edersiniz:

[email protected]

# Job adı, max süre (dakika), komut
0 2 * * * HEALTHCHECK_URL=https://hc-ping.com/UUID /opt/scripts/monitored_cron.sh "nightly_backup" 30 "/opt/scripts/backup.sh"
0 3 * * 0 /opt/scripts/monitored_cron.sh "weekly_report" 15 "/opt/scripts/generate_report.sh"
*/5 * * * * /opt/scripts/monitored_cron.sh "health_check" 2 "/opt/scripts/check_services.sh"

Sonuç

Cron görevlerini izlemek, onları yazmak kadar önemlidir. “Çalıştır ve unut” yaklaşımı, er ya da geç ciddi sorunlara yol açar. Bu yazıda anlattığımız katmanlı yaklaşımı özetlersek:

  • Log yönetimi ile her job’ın ne zaman başladığını, ne zaman bittiğini ve ne hata ürettiğini kayıt altına alın
  • Exit code takibi yaparak gerçek başarı kriterini belirleyin
  • Kilitleme mekanizması ile çakışan job instance’larını önleyin
  • Heartbeat/healthcheck sistemiyle çalışmayan job’ları proaktif tespit edin
  • Merkezi izleme ile çok sunuculu ortamları tek noktadan yönetin
  • Ortam değişkenleri ve zaman dilimi gibi sinsi sorunlara karşı savunmacı scripting yapın

Üretim ortamında cron job yönetimi için bir adım daha ileri gitmek isteyenler, Hashicorp Nomad, Kubernetes CronJob veya Apache Airflow gibi araçlara da bakabilir. Ancak klasik cron kullanmaya devam edecekseniz, bu yazıdaki teknikler sizi büyük çoğunlukta yaşanan sorunlardan koruyacaktır. Unutmayın, iyi bir sysadmin sorun çözmez, sorunların yaşanmasını engeller.

Yorum yapın