Başarısız Servisleri Otomatik Bildirme: Systemd OnFailure Kullanımı

Gece 2’de telefon çalıyor, kritik bir servis çökmüş ve sen saatlerdir haberdar değilsin. Bu senaryo her sistem yöneticisinin kabusu. Neyse ki systemd, tam da bu iş için OnFailure direktifini sunuyor. Bu yazıda OnFailure‘ı nasıl kullanacağını, gerçek dünya senaryolarında nasıl yapılandıracağını ve kurumsal ortamlarda nasıl işe yarar bir bildirim altyapısı kuracağını ele alacağız.

OnFailure Nedir ve Nasıl Çalışır?

Systemd servis dosyalarında [Unit] bölümü altında kullanılan OnFailure direktifi, bir servis başarısız olduğunda otomatik olarak başka bir servis ya da unit’i tetikler. Temel mantığı şu: izlediğin servis failed durumuna düştüğünde, OnFailure ile belirttiğin unit devreye girer.

Bir servisin “başarısız” sayılması için birkaç durum söz konusu olabilir:

  • Non-zero exit code: Servis sıfır olmayan bir çıkış kodu döndürmüş
  • Sinyal ile sonlanma: Servis bir sinyal tarafından öldürülmüş (örneğin OOM killer)
  • Zaman aşımı: TimeoutStartSec ya da TimeoutStopSec dolmuş
  • Watchdog timeout: Watchdog ping’i zamanında gelmemiş
  • Start limit: StartLimitIntervalSec içinde çok fazla yeniden başlatma denemesi

Bu durumların tümünde OnFailure tetiklenir. Restart=always veya Restart=on-failure ile birlikte kullanıldığında dikkatli olmak gerekiyor; servis sürekli yeniden başlatılıyorsa ve StartLimitBurst aşılmadıkça failed durumuna geçmiyor, dolayısıyla OnFailure tetiklenmiyor. Bu nüansı aklında tut.

Temel Yapılandırma

En basit haliyle bir OnFailure kurulumu şu şekilde görünür:

# /etc/systemd/system/web-app.service
[Unit]
Description=Web Uygulamamız
OnFailure=notify-failure@%n.service

[Service]
Type=simple
ExecStart=/usr/bin/node /opt/webapp/server.js
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3

[Install]
WantedBy=multi-user.target

Buradaki %n bir systemd specifier’ı. Servisin tam adını temsil eder. Yani web-app.service başarısız olursa, [email protected] adlı servis tetiklenir. Bu template (şablon) servis mekanizması sayesinde tek bir bildirim servisiyle tüm servisleri izleyebilirsin.

Bildirim Servisi Oluşturma

Basit E-posta Bildirimi

Şimdi tetiklenecek olan bildirim servisini oluşturalım. Template servis dosyası @ işareti içerir:

# /etc/systemd/system/[email protected]
[Unit]
Description=Servis Başarısızlık Bildirimi - %i
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure.sh %i
User=root

Görüldüğü gibi bu bir Type=oneshot servis. Tek bir komut çalıştırır, biter. %i ise template instance adını, yani başarısız olan servisin adını temsil eder.

Şimdi bildirim script’ini yazalım:

#!/bin/bash
# /usr/local/bin/notify-failure.sh

SERVICE_NAME="$1"
HOSTNAME=$(hostname -f)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
RECIPIENT="[email protected]"
SMTP_SERVER="mail.sirket.com"

# Servis durumu hakkında detaylı bilgi topla
SERVICE_STATUS=$(systemctl status "$SERVICE_NAME" 2>&1 | head -50)
JOURNAL_LOGS=$(journalctl -u "$SERVICE_NAME" -n 30 --no-pager 2>&1)

# E-posta içeriği oluştur
MAIL_BODY="KRITIK: $SERVICE_NAME servisi basarisiz oldu!

Sunucu: $HOSTNAME
Zaman: $TIMESTAMP
Servis: $SERVICE_NAME

=== SERVIS DURUMU ===
$SERVICE_STATUS

=== SON LOG SATIRLARI ===
$JOURNAL_LOGS
"

# E-posta gönder
echo "$MAIL_BODY" | mail -s "[ALARM] $SERVICE_NAME - $HOSTNAME sunucusunda basarisiz" 
    -r "[email protected]" 
    "$RECIPIENT"

echo "Bildirim gonderildi: $SERVICE_NAME @ $TIMESTAMP"

Script’i çalıştırılabilir hale getirmeyi unutma:

chmod +x /usr/local/bin/notify-failure.sh
systemctl daemon-reload

Gerçek Dünya Senaryoları

Senaryo 1: Kritik Veritabanı Servisi İzleme

Üretim ortamında PostgreSQL çalıştırıyorsun ve bu servis düşerse hem DBA ekibine hem de nöbetçi sistem yöneticisine aynı anda haber vermek istiyorsun:

# /etc/systemd/system/postgresql.service.d/failure-notify.conf
[Unit]
OnFailure=db-critical-alert@%n.service

Mevcut bir servis dosyasını override etmek için drop-in directory kullanmak en temiz yöntem. Böylece paket güncellemelerinde orijinal dosya bozulmaz.

# Drop-in dizinini oluştur
mkdir -p /etc/systemd/system/postgresql.service.d/

# Override dosyasını oluştur
cat > /etc/systemd/system/postgresql.service.d/failure-notify.conf << 'EOF'
[Unit]
OnFailure=db-critical-alert@%n.service
EOF

Veritabanı kritik alert servisi:

# /etc/systemd/system/[email protected]
[Unit]
Description=Veritabanı Kritik Alert - %i
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-critical-alert.sh %i
User=root
Environment=ALERT_LEVEL=CRITICAL
Environment=TEAM_WEBHOOK=https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ
[email protected]
[email protected]
#!/bin/bash
# /usr/local/bin/db-critical-alert.sh

SERVICE_NAME="$1"
HOSTNAME=$(hostname -f)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
UPTIME=$(uptime -p)

# Slack webhook bildirimi
send_slack() {
    local message="$1"
    curl -s -X POST "$TEAM_WEBHOOK" 
        -H 'Content-type: application/json' 
        --data "{
            "text": "*:rotating_light: KRİTİK VERİTABANI ALARMI :rotating_light:*",
            "attachments": [{
                "color": "danger",
                "fields": [
                    {"title": "Servis", "value": "$SERVICE_NAME", "short": true},
                    {"title": "Sunucu", "value": "$HOSTNAME", "short": true},
                    {"title": "Zaman", "value": "$TIMESTAMP", "short": true},
                    {"title": "Uptime", "value": "$UPTIME", "short": true},
                    {"title": "Detay", "value": "$message", "short": false}
                ]
            }]
        }" > /dev/null 2>&1
}

# Son hata logu al
LAST_ERROR=$(journalctl -u "$SERVICE_NAME" -n 10 --no-pager -p err 2>&1)

# Hem Slack'e hem e-postaya gönder
send_slack "Servis başarısız oldu. Son hatalar:n$LAST_ERROR"

echo "KRİTİK ALARM - $SERVICE_NAME başarısız oldu.nnSunucu: $HOSTNAMEnZaman: $TIMESTAMPnnSon Hatalar:n$LAST_ERROR" | 
    mail -s "[KRİTİK] PostgreSQL ÇÖKTÜ - $HOSTNAME" 
    "$DBA_EMAIL" "$ONCALL_EMAIL"

# Alert log dosyasına kaydet
echo "$TIMESTAMP - $SERVICE_NAME FAILED on $HOSTNAME" >> /var/log/service-failures.log

Senaryo 2: Nginx Web Sunucusu ile Otomatik Kurtarma Girişimi

Bazen sadece bildirim yetmez, önce kurtarma denemesi yapıp sonra bildirim göndermek istersin:

# /etc/systemd/system/nginx.service.d/auto-recover.conf
[Unit]
OnFailure=nginx-recover@%n.service
# /etc/systemd/system/[email protected]
[Unit]
Description=Nginx Otomatik Kurtarma - %i
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/nginx-recover.sh %i
User=root
#!/bin/bash
# /usr/local/bin/nginx-recover.sh

SERVICE_NAME="$1"
HOSTNAME=$(hostname -f)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
MAX_RECOVER_ATTEMPTS=3
RECOVER_LOG="/var/log/nginx-recovery.log"
ALERT_EMAIL="[email protected]"

log() {
    echo "[$TIMESTAMP] $1" | tee -a "$RECOVER_LOG"
}

log "Nginx başarısız algılandı. Kurtarma başlatılıyor..."

# Nginx konfigürasyonunu test et
if nginx -t 2>/dev/null; then
    log "Nginx konfigürasyonu geçerli, yeniden başlatmayı deniyorum..."

    # Manuel yeniden başlatma denemesi
    if systemctl start nginx; then
        log "Nginx başarıyla yeniden başlatıldı!"
        echo "Nginx otomatik kurtarma BAŞARILI - $TIMESTAMP" | 
            mail -s "[BİLGİ] Nginx Kurtarıldı - $HOSTNAME" "$ALERT_EMAIL"
        exit 0
    else
        log "Manuel yeniden başlatma da başarısız oldu!"
    fi
else
    log "Nginx konfigürasyonunda HATA var!"
    NGINX_TEST_OUTPUT=$(nginx -t 2>&1)
    log "Konfigürasyon hatası: $NGINX_TEST_OUTPUT"

    # Yedek konfigürasyona geç
    if [ -f /etc/nginx/nginx.conf.backup ]; then
        log "Yedek konfigürasyona geçiliyor..."
        cp /etc/nginx/nginx.conf.backup /etc/nginx/nginx.conf
        systemctl start nginx && log "Yedek konfigürasyonla başlatıldı!" || log "Yedek konfigürasyonla da başlatılamadı!"
    fi
fi

# Her şey başarısız olduysa acil durum bildirimi gönder
JOURNAL_LOGS=$(journalctl -u "$SERVICE_NAME" -n 20 --no-pager 2>&1)
RECOVERY_LOG_CONTENT=$(tail -20 "$RECOVER_LOG")

{
    echo "Nginx otomatik kurtarma BAŞARISIZ oldu."
    echo "Sunucu: $HOSTNAME"
    echo "Zaman: $TIMESTAMP"
    echo ""
    echo "=== KURTARMA LOGU ==="
    echo "$RECOVERY_LOG_CONTENT"
    echo ""
    echo "=== SERVIS LOGU ==="
    echo "$JOURNAL_LOGS"
} | mail -s "[ACİL] Nginx Kurtarılamadı - $HOSTNAME - MÜDAHALE GEREKİYOR" "$ALERT_EMAIL"

Çoklu Bildirim Kanalı Yapılandırması

Kurumsal ortamlarda tek bir bildirim kanalı yetmez. Systemd, OnFailure direktifinde birden fazla unit belirtmene izin verir:

# /etc/systemd/system/production-app.service
[Unit]
Description=Production Uygulama
OnFailure=notify-slack@%n.service notify-email@%n.service notify-pagerduty@%n.service

[Service]
Type=forking
PIDFile=/var/run/production-app.pid
ExecStart=/opt/app/bin/start.sh
ExecStop=/opt/app/bin/stop.sh
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=120
StartLimitBurst=4

[Install]
WantedBy=multi-user.target

Bu yapılandırmada servis düştüğünde Slack, e-posta ve PagerDuty bildirimleri eş zamanlı olarak gönderilir.

OnFailure ile Birlikte Kullanılan Önemli Direktifler

OnFailure‘ı etkili kullanmak için birkaç direktifi birlikte düşünmek gerekiyor:

  • StartLimitIntervalSec: Bu süre içinde kaç kez başlatma denemesi yapılacağını belirler. Örneğin StartLimitIntervalSec=60 ile 60 saniye içindeki denemeleri sayar.
  • StartLimitBurst: Kaç başarısız deneme sonrası servisin failed durumuna geçeceğini belirler. StartLimitBurst=3 ile 3 denemeden sonra servis failed olur ve OnFailure tetiklenir.
  • RestartSec: Yeniden başlatmalar arasındaki bekleme süresi.
  • Restart: Hangi durumlarda yeniden başlatılacağını belirler. on-failure değeri en yaygın kullanımı.
  • FailureAction: Servis başarısız olduğunda sistem genelinde bir aksiyon almak için. poweroff, reboot, exit gibi değerler alabilir. Dikkatli kullan!
  • OnSuccess: OnFailure‘ın kardeşi, servis başarıyla tamamlandığında tetiklenir. Type=oneshot servislerle birlikte kullanışlıdır.

Systemd-notify ile Watchdog Entegrasyonu

Daha gelişmiş senaryolarda watchdog mekanizmasıyla OnFailure‘ı birleştirip servisin kilitlendiği durumlarda da bildirim alabilirsin:

# /etc/systemd/system/critical-daemon.service
[Unit]
Description=Kritik Arka Plan Servisi
OnFailure=notify-failure@%n.service

[Service]
Type=notify
ExecStart=/opt/daemon/critical-daemon
WatchdogSec=30
Restart=on-watchdog
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5

[Install]
WantedBy=multi-user.target

Bu yapılandırmada daemon 30 saniyede bir systemd’ye sinyal göndermeli. Gönderemezse servis öldürülür, Restart=on-watchdog sayesinde yeniden başlatılmaya çalışılır, başarısız olursa OnFailure devreye girer.

Bildirim Geçmişi ve Raporlama

Uzun vadeli izleme için başarısızlık geçmişini kayıt altına almak önemli. Şu script ile hem anlık bildirim hem de günlük özet rapor üretebilirsin:

#!/bin/bash
# /usr/local/bin/service-failure-tracker.sh
# Hem anlık bildirim hem de geçmiş kaydı tutar

SERVICE_NAME="$1"
FAILURE_DB="/var/lib/service-monitor/failures.log"
REPORT_THRESHOLD=5  # Günde 5'ten fazla arıza varsa haftalık rapor yerine anlık bildirim
HOSTNAME=$(hostname -f)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
DATE_ONLY=$(date '+%Y-%m-%d')

# Veritabanı dizinini oluştur
mkdir -p /var/lib/service-monitor

# Arızayı kayıt et
echo "$TIMESTAMP|$SERVICE_NAME|$HOSTNAME|$(systemctl is-failed $SERVICE_NAME)" >> "$FAILURE_DB"

# Bugünkü arıza sayısını hesapla
TODAY_FAILURES=$(grep "^$DATE_ONLY" "$FAILURE_DB" | grep "|$SERVICE_NAME|" | wc -l)

# Servis logu ve durumunu topla
SERVICE_STATUS=$(systemctl status "$SERVICE_NAME" --no-pager 2>&1)
RECENT_LOGS=$(journalctl -u "$SERVICE_NAME" -n 15 --no-pager --since "1 hour ago" 2>&1)

# Bildirim e-postası oluştur
SUBJECT="[ALARM #$TODAY_FAILURES] $SERVICE_NAME - $HOSTNAME"
if [ "$TODAY_FAILURES" -gt "$REPORT_THRESHOLD" ]; then
    SUBJECT="[TEKRARLAYAN ARIZA #$TODAY_FAILURES] $SERVICE_NAME - DIKKAT GEREKIYOR"
fi

{
    echo "Servis Adı: $SERVICE_NAME"
    echo "Sunucu: $HOSTNAME"
    echo "Zaman: $TIMESTAMP"
    echo "Bugünkü Toplam Arıza: $TODAY_FAILURES"
    echo ""
    echo "=== SERVIS DURUMU ==="
    echo "$SERVICE_STATUS"
    echo ""
    echo "=== SON 1 SAATIN LOGLARI ==="
    echo "$RECENT_LOGS"
    echo ""
    echo "=== HAFTALIK ARİZA ÖZETİ ==="
    echo "Son 7 gün içinde bu servis için kayıtlı arızalar:"
    grep "|$SERVICE_NAME|" "$FAILURE_DB" | tail -20
} | mail -s "$SUBJECT" "[email protected]"

Sık Yapılan Hatalar ve Çözümleri

OnFailure kullanırken karşılaşılan en yaygın sorunlara dikkat etmek gerekiyor:

  • Bildirim döngüsü: Bildirim servisi de başarısız olursa ve ona da OnFailure eklediysen sonsuz döngüye girebilirsin. Bildirim servislerine OnFailure ekleme.
  • Restart ve StartLimitBurst karışıklığı: Restart=on-failure ile StartLimitBurst=3 beraber kullanıldığında, OnFailure sadece limit aşıldıktan sonra tetiklenir. Bu beklenen davranış ama bazen şaşırtıcı olabilir.
  • Ağ bağımlılığı: Bildirim servisi e-posta ya da webhook gönderiyorsa After=network-online.target eklemen gerekebilir. Aksi halde ağ hazır olmadan çalışıp başarısız olabilir.
  • Template servis yanlış adlandırma: [email protected] dosya adındaki @ işaretini unutursan template çalışmaz.
  • İzin sorunları: Bildirim script’i root olmayan kullanıcıyla çalışıyorsa journalctl okuma yetkisi olmayabilir. systemd-journal grubuna ekle ya da User=root kullan.

Test Etme

Kurulumu test etmek için kasıtlı olarak başarısız bir servis oluşturabilirsin:

# Test servisi oluştur
cat > /etc/systemd/system/test-failure.service << 'EOF'
[Unit]
Description=Test Başarısızlık Servisi
OnFailure=notify-failure@%n.service

[Service]
Type=oneshot
ExecStart=/bin/false
EOF

systemctl daemon-reload

# Servisi başlat, başarısız olacak ve OnFailure tetiklenecek
systemctl start test-failure.service

# Durumu kontrol et
systemctl status test-failure.service
journalctl -u [email protected] --no-pager

/bin/false her zaman 1 (başarısız) çıkış kodu döner, dolayısıyla test-failure.service hemen failed durumuna geçer ve [email protected]‘i tetikler.

Sonuç

OnFailure direktifi, systemd’nin en pratik ve değer katan özelliklerinden biri. Doğru kurulduğunda gece 2’deki o kabus telefon çalması yerini proaktif bir bildirim sistemine bırakır. Burada anlattıklarımı kısaca özetlemek gerekirse:

OnFailure ile template servis yapısını (@ notasyonu) birleştirerek tek bir bildirim mekanizmasıyla tüm kritik servislerini izleyebilirsin. Drop-in directory kullanarak mevcut servisleri bozmadan konfigürasyon eklemek en güvenli yaklaşım. StartLimitIntervalSec ve StartLimitBurst değerlerini dikkatli ayarlamak, gereksiz alarm yağmurunun önüne geçer.

Bildirim servislerini basit tut, sadece birini değil birden fazla kanalı (e-posta, Slack, PagerDuty) hedefle ve her zaman test et. En iyi alarm sistemi, gerçekten alarm üretmeden önce test edilmiş olandır. Arıza geçmişini loglamak ise kronik sorunları tespit edip kök sebep analizinde zaman kazandırır.

Şimdi systemctl daemon-reload yap ve ilk OnFailure bildirimini kur. Bir sonraki sunucu arızasında saatler sonra değil, dakikalar içinde haberdar olacaksın.

Yorum yapın