Webhook ile Discord Bot Bildirimleri Nasıl Kurulur

Sunucu monitoring altyapısı kurarken en çok canımı sıkan şey şuydu: kritik bir alarm çaldığında, o alarmın önüne geçmesi için ayrı bir araç daha kurmak zorunda kalmak. Nagios alarm gönderdi, Zabbix alert fırlattı, ama sen o anda bilgisayar başında değilsin. E-posta mı? Kim bakıyor e-postaya artık. İşte tam bu noktada Discord webhook’ları hayat kurtarıcı oluyor. Ekibinle zaten Discord’da iletişim kuruyorsanız, neden alarm bildirimlerini de oraya taşımayasın?

Bu yazıda sıfırdan Discord webhook kurulumu yapacak, Bash ve Python ile notification script’leri yazacak, gerçek monitoring senaryolarında kullanacak ve sonunda production’da kullanılabilir bir sistem ortaya çıkaracağız.

Discord Webhook Nedir, Nasıl Çalışır?

Discord webhook’u, bir Discord kanalına dışarıdan mesaj göndermenizi sağlayan özel bir URL’dir. Bot token’ı, OAuth akışı, API anahtarı yok. Sadece o URL’e HTTP POST isteği atıyorsunuz, mesaj kanala düşüyor. Bu kadar basit.

Teknik olarak bakınca: Discord, her webhook için benzersiz bir endpoint oluşturuyor. Bu endpoint’e JSON payload gönderdiğinizde, Discord bunu ilgili kanala bot mesajı olarak basıyor. Rate limit var tabii (saniyede 5 istek, kanal başına dakikada 30 mesaj) ama monitoring amaçlı kullanım için bu limitler yeterince geniş.

Webhook URL’sinin formatı şöyle görünür:

https://discord.com/api/webhooks/{webhook.id}/{webhook.token}

Bu URL’yi kimseyle paylaşmayın. Ele geçiren herkes kanalınıza mesaj atabilir.

Discord Webhook Oluşturma

Önce Discord tarafında webhook’u oluşturmamız lazım.

  1. Discord’da bildirim göndermek istediğiniz sunucuya ve kanala gidin
  2. Kanalın yanındaki dişli ikonuna tıklayın (Kanal Ayarları)
  3. Sol menüden Entegrasyonlar seçin
  4. Webhook Oluştur butonuna tıklayın
  5. Webhook’a bir isim verin (örneğin “Server Monitor” veya “Alert Bot”)
  6. İsteğe bağlı olarak bir avatar resmi ekleyebilirsiniz
  7. Webhook URL’sini Kopyala diyerek URL’yi alın

Bu URL’yi güvenli bir yerde saklayın. Ben genellikle /etc/monitoring/discord.conf gibi bir dosyaya koyup izinlerini 600 yapıyorum.

# Webhook URL'yi config dosyasına kaydet
mkdir -p /etc/monitoring
cat > /etc/monitoring/discord.conf << 'EOF'
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"
EOF
chmod 600 /etc/monitoring/discord.conf
chown root:root /etc/monitoring/discord.conf

İlk Test: Basit curl ile Mesaj Gönderme

Teoride her şey güzel görünüyor. Hemen pratiğe geçelim ve curl ile ilk mesajı gönderelim.

#!/bin/bash
# Basit Discord webhook testi

WEBHOOK_URL="https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"

curl -H "Content-Type: application/json" 
     -d '{"content": "Merhaba! Bu bir test mesajıdır."}' 
     "$WEBHOOK_URL"

Bu çalışırsa Discord kanalınızda mesajı göreceksiniz. Ama bu çok temel. Güzel, renkli, embed formatında mesajlar göndermek için biraz daha işin içine girmek gerekiyor.

Embed Formatında Zengin Bildirimler

Discord embed’leri, mesajlarınızı çok daha okunaklı hale getirir. Renk, başlık, açıklama, alanlar, footer ekleyebilirsiniz. Monitoring için bu özellikler altın değerinde.

#!/bin/bash
# Embed formatında Discord bildirimi gönderme scripti

source /etc/monitoring/discord.conf

send_discord_alert() {
    local title="$1"
    local description="$2"
    local color="$3"  # Decimal renk kodu (kırmızı: 15158332, sarı: 16776960, yeşil: 3066993)
    local hostname=$(hostname -f)
    local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")

    local payload=$(cat <<EOF
{
  "embeds": [
    {
      "title": "$title",
      "description": "$description",
      "color": $color,
      "footer": {
        "text": "Sunucu: $hostname"
      },
      "timestamp": "$timestamp"
    }
  ]
}
EOF
)

    curl -s -o /dev/null -w "%{http_code}" 
         -H "Content-Type: application/json" 
         -d "$payload" 
         "$DISCORD_WEBHOOK_URL"
}

# Kullanım örnekleri
send_discord_alert "Disk Uyarısı" "/var partition %90 dolu!" 16776960
send_discord_alert "Kritik Hata" "MySQL servisi çöktü!" 15158332
send_discord_alert "Sistem Normale Döndü" "MySQL servisi yeniden başlatıldı." 3066993

Renk kodları için not: Discord decimal renk değeri kullanıyor. #FF0000 kırmızısı için 16711680, #FFFF00 sarısı için 16776960, #2ECC71 yeşili için 3066993 kullanabilirsiniz.

Gelişmiş Script: Çoklu Alan Desteği ile CPU/RAM/Disk Monitoring

Gerçek dünyada tek satır açıklama yetmez. Sunucunun genel durumunu özetleyen, birden fazla metrik içeren bildirimler göndermek istiyoruz.

#!/bin/bash
# /usr/local/bin/discord-monitor.sh
# Sunucu kaynak kullanımını Discord'a gönderir

set -euo pipefail

source /etc/monitoring/discord.conf

# Eşik değerleri
CPU_THRESHOLD=80
MEM_THRESHOLD=85
DISK_THRESHOLD=90

HOSTNAME=$(hostname -f)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
ALERT_TRIGGERED=false
ALERT_COLOR=3066993  # Varsayılan yeşil

# Metrikleri topla
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 | cut -d',' -f1)
CPU_USAGE=${CPU_USAGE%.*}  # Float'tan integer'a

MEM_TOTAL=$(free -m | awk 'NR==2{print $2}')
MEM_USED=$(free -m | awk 'NR==2{print $3}')
MEM_PERCENT=$((MEM_USED * 100 / MEM_TOTAL))

DISK_INFO=$(df -h / | awk 'NR==2{print $5 " (" $3 "/" $2 ")"}')
DISK_PERCENT=$(df / | awk 'NR==2{print $5}' | tr -d '%')

LOAD_AVG=$(uptime | awk -F'load average:' '{print $2}' | xargs)

# Alert durumunu belirle
if [ "$CPU_USAGE" -gt "$CPU_THRESHOLD" ] || 
   [ "$MEM_PERCENT" -gt "$MEM_THRESHOLD" ] || 
   [ "$DISK_PERCENT" -gt "$DISK_THRESHOLD" ]; then
    ALERT_TRIGGERED=true
    ALERT_COLOR=15158332  # Kırmızı
fi

# CPU ve RAM için emoji belirleme
get_status_emoji() {
    local value=$1
    local threshold=$2
    if [ "$value" -gt "$threshold" ]; then
        echo "🔴"
    elif [ "$value" -gt $((threshold - 15)) ]; then
        echo "🟡"
    else
        echo "🟢"
    fi
}

CPU_EMOJI=$(get_status_emoji "$CPU_USAGE" "$CPU_THRESHOLD")
MEM_EMOJI=$(get_status_emoji "$MEM_PERCENT" "$MEM_THRESHOLD")
DISK_EMOJI=$(get_status_emoji "$DISK_PERCENT" "$DISK_THRESHOLD")

TITLE="Sunucu Durum Raporu"
if [ "$ALERT_TRIGGERED" = true ]; then
    TITLE="⚠️ Sunucu Uyarısı Tetiklendi"
fi

# Payload oluştur ve gönder
curl -s -X POST "$DISCORD_WEBHOOK_URL" 
  -H "Content-Type: application/json" 
  -d "{
    "embeds": [{
      "title": "$TITLE",
      "color": $ALERT_COLOR,
      "fields": [
        {"name": "${CPU_EMOJI} CPU Kullanımı", "value": "%${CPU_USAGE}", "inline": true},
        {"name": "${MEM_EMOJI} RAM Kullanımı", "value": "%${MEM_PERCENT} (${MEM_USED}MB/${MEM_TOTAL}MB)", "inline": true},
        {"name": "${DISK_EMOJI} Disk Kullanımı", "value": "${DISK_INFO}", "inline": true},
        {"name": "⚙️ Load Average", "value": "${LOAD_AVG}", "inline": false}
      ],
      "footer": {"text": "${HOSTNAME}"},
      "timestamp": "${TIMESTAMP}"
    }]
  }"

echo "Bildirim gönderildi. Alert: $ALERT_TRIGGERED"

Python ile Daha Güçlü Webhook Entegrasyonu

Bash iş görür ama karmaşık senaryolarda Python çok daha rahat. Özellikle hata yönetimi, retry mekanizması ve birden fazla webhook’a yönlendirme gibi durumlarda Python tercih ediyorum.

#!/usr/bin/env python3
# /usr/local/lib/discord_notify.py
# Production'a hazır Discord webhook modülü

import json
import time
import urllib.request
import urllib.error
from datetime import datetime, timezone

class DiscordWebhook:
    def __init__(self, webhook_url, max_retries=3, retry_delay=5):
        self.webhook_url = webhook_url
        self.max_retries = max_retries
        self.retry_delay = retry_delay

    def send(self, title, description, color=3066993, fields=None, mention_role=None):
        """
        Discord'a embed mesaj gönderir.
        color: 15158332 (kırmızı), 16776960 (sarı), 3066993 (yeşil)
        """
        timestamp = datetime.now(timezone.utc).isoformat()

        embed = {
            "title": title,
            "description": description,
            "color": color,
            "timestamp": timestamp,
            "footer": {"text": "Monitoring System"}
        }

        if fields:
            embed["fields"] = [
                {"name": k, "value": str(v), "inline": True}
                for k, v in fields.items()
            ]

        payload = {"embeds": }

        if mention_role:
            payload["content"] = f"<@&{mention_role}>"

        return self._post_with_retry(payload)

    def send_critical(self, title, description, fields=None):
        """Kritik alertler için - @here mention ile kırmızı embed"""
        payload = {
            "content": "@here Kritik alarm!",
            "embeds": [{
                "title": f"🚨 {title}",
                "description": description,
                "color": 15158332,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "fields": [{"name": k, "value": str(v), "inline": True}
                           for k, v in (fields or {}).items()]
            }]
        }
        return self._post_with_retry(payload)

    def _post_with_retry(self, payload):
        data = json.dumps(payload).encode('utf-8')
        headers = {'Content-Type': 'application/json'}

        for attempt in range(1, self.max_retries + 1):
            try:
                req = urllib.request.Request(
                    self.webhook_url,
                    data=data,
                    headers=headers,
                    method='POST'
                )
                with urllib.request.urlopen(req, timeout=10) as response:
                    return response.status == 204

            except urllib.error.HTTPError as e:
                if e.code == 429:  # Rate limit
                    retry_after = int(e.headers.get('Retry-After', self.retry_delay))
                    print(f"Rate limit! {retry_after}s bekleniyor...")
                    time.sleep(retry_after)
                elif attempt == self.max_retries:
                    print(f"Webhook başarısız (HTTP {e.code}): {e.reason}")
                    return False
                else:
                    time.sleep(self.retry_delay)

            except Exception as e:
                if attempt == self.max_retries:
                    print(f"Webhook gönderilemedi: {e}")
                    return False
                time.sleep(self.retry_delay)

        return False


# Kullanım örneği
if __name__ == "__main__":
    import configparser

    config = configparser.ConfigParser()
    config.read('/etc/monitoring/discord.conf')

    webhook_url = config.get('DEFAULT', 'DISCORD_WEBHOOK_URL',
                             fallback='https://discord.com/api/webhooks/...')

    notifier = DiscordWebhook(webhook_url)

    notifier.send(
        title="Backup Tamamlandı",
        description="Gece yedeklemesi başarıyla tamamlandı.",
        color=3066993,
        fields={
            "Boyut": "42.3 GB",
            "Süre": "23 dakika",
            "Hedef": "s3://backups/prod"
        }
    )

Systemd Service Başarısız Olduğunda Otomatik Bildirim

Bu senaryoyu çok seviyorum. Herhangi bir systemd servisi çöktüğünde otomatik Discord bildirimi almak için systemd’nin OnFailure özelliğini kullanıyoruz.

# /usr/local/bin/discord-systemd-notify.sh
# systemd OnFailure ile kullanım için

#!/bin/bash
source /etc/monitoring/discord.conf

UNIT_NAME="$1"
HOSTNAME=$(hostname -f)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")

# Servis loglarından son 10 satırı al
LAST_LOGS=$(journalctl -u "$UNIT_NAME" -n 10 --no-pager -o short 2>/dev/null | 
            sed 's/"/\"/g' | tr 'n' '|' | sed 's/|/\n/g')

curl -s -X POST "$DISCORD_WEBHOOK_URL" 
  -H "Content-Type: application/json" 
  -d "{
    "embeds": [{
      "title": "🔴 Servis Çöktü: ${UNIT_NAME}",
      "description": "**${HOSTNAME}** üzerinde **${UNIT_NAME}** servisi başarısız oldu.",
      "color": 15158332,
      "fields": [
        {"name": "Son Loglar", "value": "```${LAST_LOGS}```", "inline": false}
      ],
      "timestamp": "${TIMESTAMP}"
    }]
  }"

Bu script’i systemd ile entegre etmek için özel bir servis dosyası oluşturuyoruz:

# /etc/systemd/system/[email protected]
# Bu dosyayı oluşturduktan sonra 'systemctl daemon-reload' çalıştırın

[Unit]
Description=Discord Failure Notification for %i
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/discord-systemd-notify.sh %i

Artık herhangi bir servis dosyasına şu satırı eklemeniz yeterli:

# İzlemek istediğiniz servisin .service dosyasına ekleyin
# Örnek: /etc/systemd/system/nginx.service veya override dosyası

[Unit]
OnFailure=discord-notify@%n.service

Override için daha temiz yaklaşım:

# nginx için override oluştur
mkdir -p /etc/systemd/system/nginx.service.d/
cat > /etc/systemd/system/nginx.service.d/discord-notify.conf << 'EOF'
[Unit]
OnFailure=discord-notify@%n.service
EOF

systemctl daemon-reload

Crontab ile Periyodik Raporlama

Sabah geldiğinizde geceye ait özet raporu Discord’da görmek güzel bir his. Bunun için basit bir cron job yeterli.

# /usr/local/bin/discord-daily-report.sh
#!/bin/bash

source /etc/monitoring/discord.conf

HOSTNAME=$(hostname -f)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
REPORT_DATE=$(date '+%d/%m/%Y')

# Uptime
UPTIME=$(uptime -p)

# Disk kullanımları
DISK_REPORT=$(df -h --output=target,pcent,used,size | grep -v "Filesystem" | 
              awk '{printf "%s: %s (%s/%s)n", $1, $2, $3, $4}' | 
              head -5 | sed 's/"/\"/g' | tr 'n' '|' | sed 's/|/\n/g')

# Son 24 saatte başarısız login denemeleri
FAILED_LOGINS=$(grep "Failed password" /var/log/auth.log 2>/dev/null | 
                grep "$(date '+%b %e')" | wc -l)

# Çalışmayan servisler
FAILED_SERVICES=$(systemctl list-units --state=failed --no-legend --no-pager 2>/dev/null | 
                  wc -l)

# Renk belirleme
COLOR=3066993
if [ "$FAILED_SERVICES" -gt 0 ] || [ "$FAILED_LOGINS" -gt 50 ]; then
    COLOR=16776960  # Sarı - dikkat gerektiren durum
fi

curl -s -X POST "$DISCORD_WEBHOOK_URL" 
  -H "Content-Type: application/json" 
  -d "{
    "embeds": [{
      "title": "📊 Günlük Rapor - ${REPORT_DATE}",
      "color": ${COLOR},
      "fields": [
        {"name": "🖥️ Sunucu", "value": "${HOSTNAME}", "inline": true},
        {"name": "⏱️ Uptime", "value": "${UPTIME}", "inline": true},
        {"name": "❌ Başarısız Servisler", "value": "${FAILED_SERVICES}", "inline": true},
        {"name": "🔐 Başarısız Login (24s)", "value": "${FAILED_LOGINS}", "inline": true},
        {"name": "💾 Disk Durumu", "value": "```${DISK_REPORT}```", "inline": false}
      ],
      "timestamp": "${TIMESTAMP}"
    }]
  }"

Crontab’a eklemek için:

# Her sabah 08:00'de günlük rapor
0 8 * * * /usr/local/bin/discord-daily-report.sh >> /var/log/discord-notify.log 2>&1

# Her 5 dakikada bir kaynak kontrolü (sadece alert durumunda bildirim gönderir)
*/5 * * * * /usr/local/bin/discord-monitor.sh >> /var/log/discord-notify.log 2>&1

Güvenlik ve Best Practice’ler

Production’da webhook kullanırken dikkat etmeniz gereken birkaç kritik nokta var:

  • Webhook URL’yi versiyonlama sistemine eklemeyin: .gitignore dosyanıza webhook config dosyalarını ekleyin. Bir kez GitHub’a push edip sonra token’ı rotate etmek zorunda kalmayın.
  • Config dosyası izinlerini düzgün ayarlayın: Daha önce belirttiğim gibi 600 izni ve root sahipliği şart. Script’leri root olmayan kullanıcıyla çalıştıracaksanız, o kullanıcıyı özel bir gruba alıp group izni 640 yapın.
  • Rate limit’e takılmayın: Çok sayıda sunucudan aynı webhook’a yazıyorsanız, her sunucu için ayrı webhook veya merkezi bir notification proxy kullanın. Dakikada 30 mesaj limiti küçük ekipler için yeterli ama büyük infrastructure’da aşılabilir.
  • Log tutun: Her webhook isteğinin sonucunu loglamak, sorun yaşandığında debugging için çok değerli.
  • Alert fatigue’e dikkat edin: Her 5 dakikada bir aynı alarmı göndermeyin. Bir alert tetiklendikten sonra, aynı alarm için en az 30 dakika bekleyin. Bunun için basit bir lock dosyası mekanizması kurabilirsiniz.
# Lock dosyası ile alert deduplication
LOCK_DIR="/tmp/discord-alerts"
mkdir -p "$LOCK_DIR"

send_deduplicated_alert() {
    local alert_key="$1"
    local cooldown="${2:-1800}"  # Varsayılan 30 dakika
    local lock_file="$LOCK_DIR/${alert_key}.lock"

    if [ -f "$lock_file" ]; then
        local last_sent=$(cat "$lock_file")
        local now=$(date +%s)
        local diff=$((now - last_sent))

        if [ "$diff" -lt "$cooldown" ]; then
            echo "Alert '$alert_key' cooldown'da, atlanıyor ($diff/$cooldown saniye)"
            return 0
        fi
    fi

    # Alert gönder
    # ... send_discord_alert çağrısı ...

    date +%s > "$lock_file"
    echo "Alert '$alert_key' gönderildi, cooldown başlatıldı."
}

Webhook’u Test Etme ve Sorun Giderme

Webhook’unuz çalışmıyorsa aşağıdaki kontrol listesini takip edin:

  • HTTP 401: Webhook URL yanlış veya token geçersiz. Discord panelinden yeni bir URL kopyalayın.
  • HTTP 404: Webhook silinmiş. Discord’dan yeni bir tane oluşturun.
  • HTTP 429: Rate limit’e takıldınız. Retry-After header’ına bakın.
  • HTTP 400: JSON payload bozuk. curl ile manuel test ederken JSON’ı önce bir validator’dan geçirin.

Hızlı debug için verbose curl:

# Verbose mod ile webhook testi
curl -v -X POST "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN" 
  -H "Content-Type: application/json" 
  -d '{"content": "Test mesajı", "embeds": [{"title": "Test", "color": 3066993, "description": "Bu bir test"}]}'

Response 204 dönüyorsa her şey yolunda. Discord, başarılı webhook isteklerinde body döndürmez, sadece 204 No Content.

Sonuç

Discord webhook entegrasyonu, monitoring altyapısına eklediğinizde ekibinizin sistem durumuna olan farkındalığı dramatik şekilde artıyor. Üç ay önce bu kurulumu yaptıktan sonra, ekibimde “sunucu ne zaman çökmüş ki?” diye sorulmaz oldu. Herkes telefona bakıyor, bildirim geliyor, beş dakika içinde müdahale ediliyor.

Başlangıç için önerdiğim yol haritası şu şekilde: önce basit curl tabanlı test scriptini çalıştırın, sonra systemd OnFailure entegrasyonunu kurun, ardından günlük raporu crontab’a ekleyin. Bu üçü bile sizi çok daha rahat bir noktaya taşır.

Webhook URL’lerinizi güvende tutun, rate limit’e dikkat edin, alert fatigue’i önlemek için cooldown mekanizması ekleyin. Bu üç kurala uyarsanız, aylar geçtikçe sizi hiç rahatsız etmeyen ama gerektiğinde hayat kurtaran bir bildirim sisteminiz olur.

Sorularınız varsa veya farklı entegrasyon senaryolarında takıldıysanız, yorum bölümünde yazın. Bir sonraki yazıda Grafana alerting’i de Discord’a bağlamayı planlıyorum.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir