Webhook ile Monitoring Sistemi Entegrasyonu

Monitoring sistemleri güzel çalışıyor, alert’ler geliyor, dashboard’lar yeşil yanıyor… ama bir problem var: her şey izole. Grafana kendi köşesinde, Prometheus kendi dünyasında, Slack kanalınız da tamamen habersiz. İşte tam bu noktada webhook entegrasyonu devreye giriyor ve monitoring stack’inizi gerçekten konuşan bir sisteme dönüştürüyor.

Bu yazıda production ortamında webhook tabanlı monitoring entegrasyonunu nasıl kurduğumu, hangi hatalarla karşılaştığımı ve nasıl aştığımı anlatacağım. Teorik değil, elleri kirletilmiş notlar bunlar.

Webhook Mantığını Anlamak

Webhook, en basit tanımıyla “bir olay gerçekleştiğinde şu URL’e HTTP POST at” demek. Polling’in tersine, siz sistemi sürekli sorgulamak yerine sistem sizi bilgilendiriyor. Monitoring bağlamında bu şu anlama geliyor: CPU %95’e çıktığında Grafana beklemeden Slack’e, PagerDuty’e veya kendi yazdığınız bir servise haber gönderiyor.

Basit görünüyor, ama üretim ortamında bunu güvenilir hale getirmek ciddi iş. Retry mekanizmaları, secret doğrulama, payload şeması, rate limiting… hepsini düşünmek gerekiyor.

Temel Webhook Alıcı Servisi Yazmak

İlk adım olarak kendi webhook alıcınızı yazın. Bu sayede neyin geldiğini tam anlarsınız. Ben genellikle Python Flask ile başlıyorum:

pip install flask requests python-dotenv
cat > webhook_receiver.py << 'EOF'
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import logging
from datetime import datetime

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

WEBHOOK_SECRET = "supersecretkey123"

def verify_signature(payload_body, signature_header):
    """Grafana veya Alertmanager'dan gelen imzayi dogrula"""
    if not signature_header:
        return False
    expected = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        payload_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature_header)

@app.route('/webhook/alerts', methods=['POST'])
def receive_alert():
    # Imza dogrulama
    signature = request.headers.get('X-Grafana-Signature', '')
    if not verify_signature(request.data, signature):
        logger.warning("Gecersiz imza, istek reddedildi")
        return jsonify({"error": "Unauthorized"}), 401

    payload = request.get_json()
    logger.info(f"Alert alindi: {json.dumps(payload, indent=2)}")

    # Alert islemini buradan tetikle
    process_alert(payload)

    return jsonify({"status": "received"}), 200

def process_alert(payload):
    alerts = payload.get('alerts', [])
    for alert in alerts:
        status = alert.get('status', 'unknown')
        name = alert.get('labels', {}).get('alertname', 'Bilinmiyor')
        severity = alert.get('labels', {}).get('severity', 'info')
        logger.info(f"[{severity.upper()}] {name} - Durum: {status}")

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=False)
EOF

Bu servisi systemd ile ayağa kaldırmak en temizi:

cat > /etc/systemd/system/webhook-receiver.service << 'EOF'
[Unit]
Description=Webhook Receiver Service
After=network.target

[Service]
Type=simple
User=webhook
WorkingDirectory=/opt/webhook
ExecStart=/usr/bin/python3 /opt/webhook/webhook_receiver.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now webhook-receiver

Grafana Alerting’i Webhook’a Bağlamak

Grafana 9+ ile Unified Alerting kullanıyorsanız yapılandırma şöyle:

Contact Points bölümünden yeni bir webhook contact point ekleyin. URL olarak http://your-server:5001/webhook/alerts girin.

Ama önemli bir nokta: Grafana’nın gönderdiği payload formatını tam bilmeden iş yapmaya çalışmak vakit kaybı. Önce bir test isteği yapın:

curl -X POST http://localhost:5001/webhook/alerts 
  -H "Content-Type: application/json" 
  -H "X-Grafana-Signature: test" 
  -d '{
    "receiver": "webhook-test",
    "status": "firing",
    "alerts": [
      {
        "status": "firing",
        "labels": {
          "alertname": "HighCPU",
          "severity": "critical",
          "instance": "web-server-01:9100"
        },
        "annotations": {
          "summary": "CPU kullanimi yuzde 95 ustu",
          "description": "web-server-01 makinesi son 5 dakikadır yuzde 95 uzerinde CPU kullaniyor"
        },
        "startsAt": "2024-01-15T10:30:00Z"
      }
    ]
  }'

Grafana tarafında da Notification Policies kısmında bu contact point’i doğru alert’lere yönlendirdiğinizden emin olun. Severity etiketine göre routing yapmak çok işe yarıyor.

Prometheus Alertmanager ile Webhook Entegrasyonu

Alertmanager doğrudan Prometheus alert’lerini webhook’a iletmek için çok daha güçlü bir araç. alertmanager.yml yapılandırması:

cat > /etc/alertmanager/alertmanager.yml << 'EOF'
global:
  resolve_timeout: 5m
  http_config:
    follow_redirects: true

route:
  receiver: 'default-webhook'
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: critical
      receiver: 'critical-webhook'
      continue: true
    - match:
        severity: warning
      receiver: 'warning-webhook'

receivers:
  - name: 'default-webhook'
    webhook_configs:
      - url: 'http://localhost:5001/webhook/alerts'
        send_resolved: true
        http_config:
          bearer_token: 'your-bearer-token'
        max_alerts: 10

  - name: 'critical-webhook'
    webhook_configs:
      - url: 'http://localhost:5001/webhook/critical'
        send_resolved: true
        timeout: 10s

  - name: 'warning-webhook'
    webhook_configs:
      - url: 'http://localhost:5001/webhook/warnings'
        send_resolved: false

inhibit_rules:
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'instance']
EOF

alertmanager --config.file=/etc/alertmanager/alertmanager.yml --check-config

inhibit_rules kısmı çok kritik. Critical bir alert varken aynı instance için warning gönderme demek. Yoksa alarm bombardımanına tutulursunuz.

Slack Entegrasyonu: Pratik Yaklaşım

Slack incoming webhook URL’inizi aldıktan sonra webhook alıcınızı Slack’e bağlayın. Ben bu işi doğrudan Alertmanager üzerinden değil, kendi servisim üzerinden yapıyorum çünkü mesajı özelleştirme ihtiyacı kaçınılmaz oluyor:

cat > slack_notifier.py << 'EOF'
import requests
import json
from datetime import datetime

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ"

SEVERITY_COLORS = {
    "critical": "#FF0000",
    "warning":  "#FFA500",
    "info":     "#36a64f",
    "resolved": "#2ecc71"
}

SEVERITY_EMOJIS = {
    "critical": ":red_circle:",
    "warning":  ":large_yellow_circle:",
    "info":     ":large_green_circle:",
    "resolved": ":white_check_mark:"
}

def send_slack_alert(alert_data):
    status = alert_data.get('status', 'unknown')
    labels = alert_data.get('labels', {})
    annotations = alert_data.get('annotations', {})

    severity = labels.get('severity', 'info')
    if status == 'resolved':
        severity = 'resolved'

    alertname = labels.get('alertname', 'Bilinmeyen Alert')
    instance = labels.get('instance', 'Bilinmiyor')
    summary = annotations.get('summary', 'Detay yok')
    description = annotations.get('description', '')

    emoji = SEVERITY_EMOJIS.get(severity, ':question:')
    color = SEVERITY_COLORS.get(severity, '#888888')

    payload = {
        "attachments": [
            {
                "color": color,
                "blocks": [
                    {
                        "type": "header",
                        "text": {
                            "type": "plain_text",
                            "text": f"{emoji} {alertname}"
                        }
                    },
                    {
                        "type": "section",
                        "fields": [
                            {
                                "type": "mrkdwn",
                                "text": f"*Durum:*n{status.upper()}"
                            },
                            {
                                "type": "mrkdwn",
                                "text": f"*Sunucu:*n{instance}"
                            },
                            {
                                "type": "mrkdwn",
                                "text": f"*Ozet:*n{summary}"
                            },
                            {
                                "type": "mrkdwn",
                                "text": f"*Zaman:*n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
                            }
                        ]
                    }
                ]
            }
        ]
    }

    if description:
        payload["attachments"][0]["blocks"].append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Detay:*n{description}"
            }
        })

    response = requests.post(
        SLACK_WEBHOOK_URL,
        data=json.dumps(payload),
        headers={'Content-Type': 'application/json'},
        timeout=10
    )

    if response.status_code != 200:
        raise Exception(f"Slack isteği başarisiz: {response.status_code}, {response.text}")

    return True
EOF

Retry ve Güvenilirlik Mekanizması

Production’da webhook’ların kaybolması gerçek bir problem. Karşı taraf geçici olarak erişilemez olabilir, timeout olabilir. Bu yüzden retry mekanizması şart:

cat > reliable_webhook.py << 'EOF'
import time
import requests
import logging
from functools import wraps

logger = logging.getLogger(__name__)

def retry_with_backoff(max_retries=3, backoff_factor=2, exceptions=(requests.exceptions.RequestException,)):
    """Exponential backoff ile retry decorator"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    wait_time = backoff_factor ** attempt
                    logger.warning(
                        f"Deneme {attempt + 1}/{max_retries} başarisiz: {e}. "
                        f"{wait_time} saniye bekleniyor..."
                    )
                    time.sleep(wait_time)
            logger.error(f"Tum denemeler bitti. Son hata: {last_exception}")
            raise last_exception
        return wrapper
    return decorator

@retry_with_backoff(max_retries=3, backoff_factor=2)
def send_webhook(url, payload, headers=None, timeout=10):
    """Guvenilir webhook gonderici"""
    default_headers = {
        'Content-Type': 'application/json',
        'User-Agent': 'MonitoringWebhook/1.0'
    }
    if headers:
        default_headers.update(headers)

    response = requests.post(
        url,
        json=payload,
        headers=default_headers,
        timeout=timeout
    )

    # 5xx hatalari retry icin exception firlatir
    if response.status_code >= 500:
        raise requests.exceptions.HTTPError(
            f"Server hatasi: {response.status_code}"
        )

    # 4xx hatalari retry yapma, dogrudan logla
    if response.status_code >= 400:
        logger.error(f"Client hatasi {response.status_code}: {response.text}")
        return False

    return True
EOF

Nginx ile Webhook Proxy ve Rate Limiting

Webhook alıcınızı doğrudan internete açmak yerine Nginx arkasına alın ve rate limiting ekleyin. Bu hem güvenlik hem de DDoS koruması açısından önemli:

cat > /etc/nginx/sites-available/webhook << 'EOF'
# Rate limiting zone tanimla
limit_req_zone $binary_remote_addr zone=webhook_limit:10m rate=30r/m;

upstream webhook_backend {
    server 127.0.0.1:5001;
    keepalive 8;
}

server {
    listen 443 ssl http2;
    server_name webhook.yourcompany.com;

    ssl_certificate     /etc/ssl/certs/webhook.crt;
    ssl_certificate_key /etc/ssl/private/webhook.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    # Sadece POST kabul et
    location /webhook/ {
        limit_req zone=webhook_limit burst=10 nodelay;
        limit_req_status 429;

        # Sadece bilinen IP'lerden kabul et (Grafana ve Alertmanager IP'leri)
        allow 10.0.0.10;   # Grafana sunucusu
        allow 10.0.0.11;   # Alertmanager sunucusu
        deny all;

        if ($request_method != POST) {
            return 405;
        }

        # Maksimum payload boyutu: 1MB
        client_max_body_size 1m;

        proxy_pass http://webhook_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout 5s;
        proxy_read_timeout 10s;
    }
}
EOF

nginx -t && systemctl reload nginx

Gerçek Dünya Senaryosu: Disk Dolması Otomasyonu

Sadece bildirim göndermekle kalmayan, aksiyon alan bir senaryo anlatayım. Disk %90’ı aştığında hem Slack’e bildirim gidip hem de otomatik log temizleme başlasın:

cat > /opt/webhook/handlers/disk_handler.py << 'EOF'
import subprocess
import logging
import os
from slack_notifier import send_slack_alert

logger = logging.getLogger(__name__)

def handle_disk_alert(alert):
    labels = alert.get('labels', {})
    instance = labels.get('instance', '')
    mountpoint = labels.get('mountpoint', '/')
    severity = labels.get('severity', 'warning')

    logger.info(f"Disk alert isleniyor: {instance} - {mountpoint}")

    # Once bildirimi gonder
    send_slack_alert(alert)

    # Critical ise otomatik temizlik baslat
    if severity == 'critical' and mountpoint == '/var/log':
        logger.info(f"Otomatik log temizleme basliyor: {instance}")
        cleanup_result = run_log_cleanup(instance, mountpoint)

        # Temizlik sonucunu raporla
        follow_up = {
            'status': 'info',
            'labels': {
                'alertname': 'AutoCleanupCompleted',
                'instance': instance,
                'severity': 'info'
            },
            'annotations': {
                'summary': 'Otomatik log temizleme tamamlandi',
                'description': f"Temizlenen alan: {cleanup_result.get('freed_mb', 0)} MB"
            }
        }
        send_slack_alert(follow_up)

def run_log_cleanup(instance, mountpoint):
    """
    Dikkat: Bunu production'da cok iyi test edin!
    Bu ornek sadece yerel makinede calisir.
    Remote cleanup icin SSH veya Ansible kullanin.
    """
    freed_mb = 0
    try:
        # 7 gunden eski compressed log'lari sil
        result = subprocess.run(
            ['find', mountpoint, '-name', '*.gz', '-mtime', '+7', '-delete', '-print'],
            capture_output=True,
            text=True,
            timeout=60
        )
        deleted_files = result.stdout.strip().split('n') if result.stdout.strip() else []
        logger.info(f"{len(deleted_files)} dosya silindi")

        # journalctl log boyutunu kisit
        subprocess.run(
            ['journalctl', '--vacuum-time=7d'],
            capture_output=True,
            timeout=30
        )

        freed_mb = len(deleted_files) * 5  # kabaca tahmin
    except subprocess.TimeoutExpired:
        logger.error("Temizleme islemi zaman asimina ugradi")
    except Exception as e:
        logger.error(f"Temizleme hatasi: {e}")

    return {'freed_mb': freed_mb}
EOF

Webhook Loglarını İzlemek

Webhook trafiğini kayıt altına almadan yönetmek kör uçmak gibi. Basit bir log rotasyonu ve analiz scripti:

cat > /opt/webhook/analyze_logs.sh << 'EOF'
#!/bin/bash
# Son 24 saatteki webhook istatistikleri

LOG_FILE="/var/log/webhook/receiver.log"
DATE=$(date -d "yesterday" +%Y-%m-%d)

echo "=== Webhook Istatistikleri: $DATE ==="
echo ""

echo "Toplam istek sayisi:"
grep "$DATE" "$LOG_FILE" | grep "Alert alindi" | wc -l

echo ""
echo "Severity dagilimi:"
grep "$DATE" "$LOG_FILE" | grep -oP 'severity.*?(w+)' | sort | uniq -c | sort -rn

echo ""
echo "En cok alert ureten instance'lar:"
grep "$DATE" "$LOG_FILE" | grep -oP 'instance": "K[^"]+' | sort | uniq -c | sort -rn | head -10

echo ""
echo "Basarisiz webhook gonderim sayisi:"
grep "$DATE" "$LOG_FILE" | grep -c "ERROR|FAILED"
EOF

chmod +x /opt/webhook/analyze_logs.sh
echo "0 8 * * * /opt/webhook/analyze_logs.sh | mail -s 'Webhook Raporu' [email protected]" | crontab -

Yaygın Sorunlar ve Çözümleri

Production’da defalarca karşılaştığım sorunları listeleyelim:

Imza dogrulaması sürekli başarısız oluyor:

  • Alertmanager ve Grafana farklı secret formatları kullanır. Hangisinin ne gönderdiğini wireshark veya tcpdump ile kontrol edin.
  • tcpdump -i lo -A port 5001 ile lokal trafiği izleyin.

Alert’ler bazen gelmiyor:

  • Timeout değerlerini artırın. Flask’ın varsayılan timeout’u çok düşük olabilir.
  • group_wait ve group_interval değerlerini Alertmanager’da gözden geçirin.
  • Webhook alıcınız bir exception fırlatıyorsa 500 döner ve Alertmanager retry yapar ama bu loglanmıyorsa fark etmezsiniz.

Duplicate alert’ler geliyor:

  • inhibit_rules tanımlarınızı kontrol edin.
  • repeat_interval değeri çok düşük ayarlanmış olabilir.
  • continue: true kullandığınız route’larda dikkatli olun, aynı alert birden fazla receiver’a gidebilir.

Rate limit aşımı:

  • Nginx’te burst değerini artırın ama dikkatli olun, bir incident sırasında onlarca alert gelebilir.
  • Alertmanager’da group_by ile alert’leri gruplandırarak tek mesaj haline getirin.

Monitoring Stack Sağlığını İzlemek

Bir de “monitoring’i kim izleyecek” sorusu var. Webhook alıcısının kendisinin çalışıp çalışmadığını kontrol etmek için Prometheus’a metric’ler ekleyin:

cat >> webhook_receiver.py << 'EOF'

from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST

WEBHOOK_RECEIVED = Counter('webhook_alerts_received_total', 'Toplam alinan alert sayisi', ['status', 'severity'])
WEBHOOK_PROCESSING_TIME = Histogram('webhook_processing_seconds', 'Alert isleme suresi')

@app.route('/metrics')
def metrics():
    return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST}

# process_alert fonksiyonunu guncelle:
def process_alert_with_metrics(payload):
    with WEBHOOK_PROCESSING_TIME.time():
        alerts = payload.get('alerts', [])
        for alert in alerts:
            status = alert.get('status', 'unknown')
            severity = alert.get('labels', {}).get('severity', 'info')
            WEBHOOK_RECEIVED.labels(status=status, severity=severity).inc()
EOF

Sonra Prometheus’a bu endpoint’i scrape ettirirsiniz ve webhook alıcınızın sağlığını da Grafana’dan izlemiş olursunuz. Döngüsel bir güzellik.

Sonuç

Webhook tabanlı monitoring entegrasyonu başta basit görünüyor: “URL’e POST at, tamam.” Ama güvenilir, güvenli ve sürdürülebilir bir sistem kurmak gerçekten dikkat istiyor.

En kritik üç nokta:

  • Imza dogrulaması hiç atlanmamalı. Secret olmayan bir webhook, internete açık bir API endpoint’inden farksız.
  • Retry mekanizması olmadan alert kaybedersiniz. Murphy yasası gereği, tam kritik bir incident sırasında karşı taraf geçici olarak erişilemez olur.
  • Alert gruplama kurulmadan alert bombardımanı kaçınılmaz. Alertmanager’ın group_by ve inhibit_rules özelliklerini iyi öğrenin.

Bu sistemi bir kez doğru kurduğunuzda, monitoring stack’iniz sadece izlemekle kalmayıp gerçekten konuşmaya ve hatta aksiyon almaya başlıyor. Gece 3’te telefonunuz çaldığında ne olduğunu bilmek, nereye bakacağınızı bilmek ve belki de sorunun yarısı zaten çözülmüş olmak… bu konforu bir kez tatttıktan sonra webhook olmadan monitoring yapmak garip geliyor.

Bir yanıt yazın

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