Elasticsearch Watcher ile Otomatik Alarm Kuralları Oluşturma

Üretim ortamında bir Elasticsearch kümesi yönetiyorsanız, bir sabah işe geldiğinizde disk dolmuş, indeks bozulmuş ya da shard sayısı patlamış halde bulmanın ne kadar can sıkıcı olduğunu bilirsiniz. İşte Watcher tam bu noktada devreye giriyor. Kibana’nın güzel dashboardları güzel, ama siz ekrana bakmadığınızda ne olduğunu kimse size söylemiyorsa o dashboardların pek de anlamı yok. Bu yazıda Elasticsearch Watcher’ı gerçek senaryolarla nasıl kullanacağınızı, production ortamlarda hangi alarmları kurmanız gerektiğini ve bunları nasıl test edeceğinizi anlatacağım.

Watcher Nedir ve Neden Önemlidir

Watcher, Elasticsearch’ün içine entegre edilmiş bir uyarı (alerting) motorudur. Temel mantığı şu: belirli aralıklarla bir sorgu çalıştırır, sonucu bir koşulla karşılaştırır ve koşul sağlanırsa bir aksiyon tetikler. E-posta atmak, Slack bildirimi göndermek, webhook çağırmak, yeni bir döküman indekslemek, hepsi mümkün.

Elastic Stack’in eski lisanslama modelinde Watcher sadece ücretli X-Pack ile geliyordu. 7.x ve sonrasında Basic lisansla birlikte gelen özellikler değişti; ancak tam Watcher kapasitesi hala Platinum/Enterprise gerektiriyor. Eğer ücretsiz alternatif arıyorsanız Kibana Alerts (şu an “Rules” adıyla geçiyor) kullanabilirsiniz, ama Watcher’ın esnekliğine yaklaşamaz. Şirket içi Elastic lisansınız varsa ya da Elastic Cloud kullanıyorsanız direkt Watcher’a geçebilirsiniz.

Watcher’ın dört temel bileşeni var:

  • Trigger: Watch’ın ne zaman çalışacağını belirler. Genellikle schedule tabanlıdır, her 5 dakikada bir çalıştır gibi.
  • Input: Elasticsearch’e sorgu atar ve veriyi çeker.
  • Condition: Input’tan gelen veriyi değerlendirir, aksiyona geçip geçmeyeceğine karar verir.
  • Actions: Koşul sağlandığında ne yapılacağını tanımlar.

Watcher API ile İlk Adımlar

Watcher’ı komut satırından yönetmek için _watcher API endpoint’ini kullanıyoruz. Önce Watcher servisinin durumunu kontrol edelim:

curl -X GET "localhost:9200/_watcher/stats?pretty" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz

Eğer Watcher başlatılmamışsa:

curl -X POST "localhost:9200/_watcher/_start" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz

Watch oluşturmak için PUT kullanıyoruz:

curl -X PUT "localhost:9200/_watcher/watch/ilk_watch" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "trigger": {
      "schedule": {
        "interval": "5m"
      }
    },
    "input": {
      "simple": {
        "mesaj": "Merhaba Watcher"
      }
    },
    "condition": {
      "always": {}
    },
    "actions": {
      "log_action": {
        "logging": {
          "text": "Watch tetiklendi: {{ctx.payload.mesaj}}"
        }
      }
    }
  }'

Bu en basit haliyle bir Watch. Şimdi gerçek işe gelelim.

Senaryo 1: Yüksek HTTP Hata Oranı Alarmı

Nginx veya Apache loglarını Elasticsearch’e gönderiyorsunuz. 5xx hatalarının son 5 dakikada belirli bir eşiği geçmesi durumunda Slack’e bildirim atmak istiyorsunuz. Bu klasik bir production senaryosu.

curl -X PUT "localhost:9200/_watcher/watch/yuksek_http_hata_orani" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "trigger": {
      "schedule": {
        "interval": "5m"
      }
    },
    "input": {
      "search": {
        "request": {
          "indices": ["nginx-logs-*"],
          "body": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "range": {
                      "@timestamp": {
                        "gte": "now-5m"
                      }
                    }
                  },
                  {
                    "range": {
                      "response": {
                        "gte": 500,
                        "lt": 600
                      }
                    }
                  }
                ]
              }
            },
            "aggs": {
              "hata_sayisi_by_host": {
                "terms": {
                  "field": "host.keyword",
                  "size": 10
                }
              }
            }
          }
        }
      }
    },
    "condition": {
      "compare": {
        "ctx.payload.hits.total.value": {
          "gte": 50
        }
      }
    },
    "actions": {
      "slack_bildirimi": {
        "webhook": {
          "scheme": "https",
          "host": "hooks.slack.com",
          "port": 443,
          "method": "post",
          "path": "/services/XXXXX/YYYYY/ZZZZZ",
          "headers": {
            "Content-Type": "application/json"
          },
          "body": "{"text": "UYARI: Son 5 dakikada {{ctx.payload.hits.total.value}} adet 5xx hatası tespit edildi! Watch: {{ctx.watch_id}}"}"
        }
      }
    }
  }'

Burada condition.compare kullandık. Son 5 dakikada 50 veya daha fazla 5xx hatası gelirse Slack bildirimi tetikleniyor. Basit ama etkili.

Senaryo 2: Disk Kullanımı Kritik Eşik Alarmı

Metricbeat ile sistem metriklerini Elasticsearch’e gönderiyorsanız bu Watch disk dolmasını erken yakalar. Özellikle log üreten sistemlerde disk dolması Elasticsearch’i tam anlamıyla mahvedebilir, bunu production’da bir kez yaşayan bir daha yaşamak istemez.

curl -X PUT "localhost:9200/_watcher/watch/disk_kullanimi_alarmi" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "trigger": {
      "schedule": {
        "cron": "0 */15 * * * ?"
      }
    },
    "input": {
      "search": {
        "request": {
          "indices": ["metricbeat-*"],
          "body": {
            "size": 0,
            "query": {
              "bool": {
                "filter": [
                  {
                    "term": {
                      "metricset.name": "filesystem"
                    }
                  },
                  {
                    "range": {
                      "@timestamp": {
                        "gte": "now-16m"
                      }
                    }
                  }
                ]
              }
            },
            "aggs": {
              "sunucular": {
                "terms": {
                  "field": "host.name",
                  "size": 50
                },
                "aggs": {
                  "maks_disk": {
                    "max": {
                      "field": "system.filesystem.used.pct"
                    }
                  },
                  "kritik_mi": {
                    "bucket_selector": {
                      "buckets_path": {
                        "kullanim": "maks_disk"
                      },
                      "script": "params.kullanim >= 0.85"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "condition": {
      "compare": {
        "ctx.payload.aggregations.sunucular.buckets": {
          "not_eq": []
        }
      }
    },
    "transform": {
      "script": {
        "source": "return ['kritik_sunucular': ctx.payload.aggregations.sunucular.buckets.collect(b -> b.key + ' (%' + Math.round(b.maks_disk.value * 100) + ')').join(', ')]"
      }
    },
    "actions": {
      "email_bildirimi": {
        "email": {
          "to": ["[email protected]"],
          "subject": "KRITIK: Disk Kullanimi %85 Üzeri Sunucular",
          "body": {
            "text": "Aşağıdaki sunucularda disk kullanımı kritik seviyeye ulaştı:nn{{ctx.payload.kritik_sunucular}}nnLütfen acil müdahale edin."
          }
        }
      }
    }
  }'

Burada bucket_selector aggregation ile Elasticsearch tarafında filtreleme yapıyoruz. Sadece %85 üzerindeki sunucular dönüyor. transform adımında ise Painless script ile okunabilir bir mesaj üretiyoruz. E-posta aksiyonu için elasticsearch.yml dosyasında mail ayarlarını yapmayı unutmayın.

Senaryo 3: Anormal Log Hacmi Tespiti

Bu biraz daha gelişmiş bir senaryo. Bir servis normalde saatte 1000-2000 log satırı üretiyorsa, bu sayı aniden 10.000’e çıkıyorsa bir şeyler ters gidiyordur. Ya bir hata döngüsüne girmiş, ya da saldırı altında.

curl -X PUT "localhost:9200/_watcher/watch/anormal_log_hacmi" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "trigger": {
      "schedule": {
        "interval": "10m"
      }
    },
    "input": {
      "chain": {
        "inputs": [
          {
            "simdi": {
              "search": {
                "request": {
                  "indices": ["uygulama-logs-*"],
                  "body": {
                    "query": {
                      "range": {
                        "@timestamp": {
                          "gte": "now-10m"
                        }
                      }
                    },
                    "aggs": {
                      "servis_bazli": {
                        "terms": {
                          "field": "service.name.keyword"
                        }
                      }
                    },
                    "size": 0
                  }
                }
              }
            }
          },
          {
            "gecen_saat": {
              "search": {
                "request": {
                  "indices": ["uygulama-logs-*"],
                  "body": {
                    "query": {
                      "range": {
                        "@timestamp": {
                          "gte": "now-70m",
                          "lte": "now-60m"
                        }
                      }
                    },
                    "size": 0
                  }
                }
              }
            }
          }
        ]
      }
    },
    "condition": {
      "script": {
        "source": "def simdi = ctx.payload.simdi.hits.total.value; def gecen = ctx.payload.gecen_saat.hits.total.value; if (gecen == 0) return false; return simdi > (gecen * 3);"
      }
    },
    "actions": {
      "pagerduty_tetikle": {
        "webhook": {
          "scheme": "https",
          "host": "events.pagerduty.com",
          "port": 443,
          "method": "post",
          "path": "/v2/enqueue",
          "headers": {
            "Content-Type": "application/json",
            "Authorization": "Token token=PAGERDUTY_API_KEY"
          },
          "body": "{"routing_key": "PAGERDUTY_INTEGRATION_KEY", "event_action": "trigger", "payload": {"summary": "Anormal log hacmi: Son 10 dakika geçen saat aynı periyodunun 3 katını geçti", "severity": "warning", "source": "elasticsearch-watcher"}}"
        }
      }
    }
  }'

chain input tipini kullandım. Hem şimdiki hem de bir saat önceki veriyi çekip condition script’inde karşılaştırıyoruz. Bir servis geçen saatin 3 katından fazla log üretiyorsa PagerDuty’e olay açılıyor. Gece yarısı telefon açılacak kadar ciddi bir senaryo bu.

Watch’ları Test Etmek

Watch oluşturunca hemen tetiklenmez, schedule’ı bekler. Production’da beklemeden test etmek için _execute API’sini kullanın:

curl -X POST "localhost:9200/_watcher/watch/yuksek_http_hata_orani/_execute" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "trigger_data": {
      "triggered_time": "now",
      "scheduled_time": "now"
    },
    "ignore_condition": true,
    "action_modes": {
      "slack_bildirimi": "simulate"
    }
  }'

ignore_condition: true ile condition’ı atlıyoruz, her zaman tetikleniyor. action_modes: simulate ile aksiyon gerçekten çalışmıyor ama ne gönderileceğini görebiliyoruz. Gerçekten çalıştırmak istiyorsanız "simulate" yerine "execute" kullanın.

Watch geçmişini görmek için:

curl -X GET "localhost:9200/.watcher-history-*/_search?pretty" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "sort": [{"result.execution_time": {"order": "desc"}}],
    "size": 10,
    "query": {
      "term": {
        "watch_id": "yuksek_http_hata_orani"
      }
    }
  }'

Bu sorgu size son 10 tetiklenmeyi döndürür. Her kayıtta watch’ın ne zaman çalıştığı, condition’ın sağlanıp sağlanmadığı ve aksiyonların sonuçları yer alır.

Throttling: Alarm Yorgunluğunu Önlemek

Bir problem devam ettiği sürece her 5 dakikada bir alarm almak istemezsiniz. Bu alarm yorgunluğuna yol açar ve gerçek alarmlar gürültünün içinde kaybolur. Throttle period bu sorunu çözer:

curl -X PUT "localhost:9200/_watcher/watch/throttle_ornek" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "trigger": {
      "schedule": {
        "interval": "1m"
      }
    },
    "input": {
      "search": {
        "request": {
          "indices": ["logs-*"],
          "body": {
            "query": {
              "bool": {
                "filter": [
                  {"term": {"level": "ERROR"}},
                  {"range": {"@timestamp": {"gte": "now-1m"}}}
                ]
              }
            },
            "size": 0
          }
        }
      }
    },
    "condition": {
      "compare": {
        "ctx.payload.hits.total.value": {
          "gte": 10
        }
      }
    },
    "throttle_period": "30m",
    "actions": {
      "bildirim": {
        "throttle_period": "1h",
        "webhook": {
          "scheme": "https",
          "host": "hooks.slack.com",
          "port": 443,
          "method": "post",
          "path": "/services/XXXXX/YYYYY/ZZZZZ",
          "headers": {
            "Content-Type": "application/json"
          },
          "body": "{"text": "Hata eşiği aşıldı: {{ctx.payload.hits.total.value}} hata/dakika"}"
        }
      }
    }
  }'

Watch seviyesinde throttle_period: 30m koyduk. Bu demek oluyor ki watch tetiklense bile 30 dakika geçmeden aynı watch tekrar aksiyon almaz. Aksiyon seviyesinde ise 1h throttle var. Watch’ın bazı aksiyonlarını daha sık, bazılarını daha seyrek tetikleyebilirsiniz bu sayede.

Watcher Yönetimini Kolaylaştıran Bash Scriptleri

Onlarca Watch yönetirken bir script olmadan iş çok zorlaşır. Basit bir Watch yönetim scripti:

#!/bin/bash

ES_HOST="localhost:9200"
ES_USER="elastic:şifreniz"
WATCH_DIR="/etc/elasticsearch/watches"

function watch_yukle() {
    local watch_adi=$1
    local watch_dosyasi="${WATCH_DIR}/${watch_adi}.json"
    
    if [ ! -f "$watch_dosyasi" ]; then
        echo "HATA: $watch_dosyasi bulunamadı"
        exit 1
    fi
    
    echo "Watch yükleniyor: $watch_adi"
    response=$(curl -s -o /dev/null -w "%{http_code}" 
        -X PUT "http://${ES_HOST}/_watcher/watch/${watch_adi}" 
        -H 'Content-Type: application/json' 
        -u "$ES_USER" 
        -d "@${watch_dosyasi}")
    
    if [ "$response" -eq 200 ] || [ "$response" -eq 201 ]; then
        echo "TAMAM: $watch_adi başarıyla yüklendi (HTTP $response)"
    else
        echo "HATA: $watch_adi yüklenemedi (HTTP $response)"
    fi
}

function tum_watchleri_listele() {
    curl -s -X GET "http://${ES_HOST}/_watcher/watch/_search" 
        -H 'Content-Type: application/json' 
        -u "$ES_USER" 
        -d '{"size": 100}' | 
        python3 -c "
import sys, json
data = json.load(sys.stdin)
for hit in data.get('hits', {}).get('hits', []):
    status = 'AKTİF' if hit['_source'].get('status', {}).get('state', {}).get('active', False) else 'PASİF'
    print(f'{status} | {hit["_id"]}')
"
}

function watch_sil() {
    local watch_adi=$1
    curl -s -X DELETE "http://${ES_HOST}/_watcher/watch/${watch_adi}" 
        -u "$ES_USER"
    echo "Watch silindi: $watch_adi"
}

case "$1" in
    yukle) watch_yukle "$2" ;;
    listele) tum_watchleri_listele ;;
    sil) watch_sil "$2" ;;
    *) echo "Kullanım: $0 {yukle|listele|sil} [watch_adi]" ;;
esac

Bu scripti /usr/local/bin/watch-manager olarak kaydedin, chmod +x verin. Watch JSON dosyalarınızı /etc/elasticsearch/watches/ altına koyun. watch-manager listele dediğinizde tüm watch’ları aktif/pasif durumuyla görürsünüz.

Production’da Dikkat Edilmesi Gereken Noktalar

Watcher kullanırken production ortamında birkaç kritik detay var:

Watch sıklığını abartmayın. Her 30 saniyede çalışan 50 watch, Elasticsearch cluster’ınıza ciddi yük bindirabilir. Özellikle karmaşık aggregation içeren watch’lar için minimum 5 dakika interval öneririm. Monitoring watch’larını üretim cluster’ında değil, ayrı bir monitoring cluster’ında çalıştırabilirsiniz.

Index pattern’larını dikkatli seçin. logs- yerine logs-2024. gibi tarih kısıtlı pattern kullanmak sorgu süresini dramatik olarak düşürür. Watcher her çalıştığında bu sorgu çalışıyor, mümkün olduğunca sınırlı tutun.

Condition script’lerinde hata yönetimi yapın. Painless script hata fırlatırsa watch o iterasyonda çalışmaz. try-catch blokları ekleyin veya mümkünse script yerine built-in condition tiplerini kullanın.

Watch geçmişini temizleyin. .watcher-history-* indeksleri hızla büyür. ILM policy ile otomatik temizlik kurabilirsiniz:

curl -X PUT "localhost:9200/_ilm/policy/watcher-history-policy" 
  -H 'Content-Type: application/json' 
  -u elastic:şifreniz 
  -d '{
    "policy": {
      "phases": {
        "delete": {
          "min_age": "7d",
          "actions": {
            "delete": {}
          }
        }
      }
    }
  }'

Alert mesajlarını anlamlı yazın. “Hata tespit edildi” gibi bir mesaj sabah 3’te uykudan uyanan ekip arkadaşınıza hiçbir şey söylemez. Hangi servis, hangi sunucu, hangi eşik aşıldı, ne yapılması gerekiyor, hepsi mesajda olsun. Müdahale rehberinin (runbook) linkini bile ekleyebilirsiniz Slack mesajına.

Kibana Üzerinden Watch Oluşturma

Her şeyi API ile yapmak zorunda değilsiniz. Kibana’da Stack Management > Alerts and Insights > Watcher menüsünden görsel bir arayüzle Watch oluşturabilirsiniz. Özellikle threshold tabanlı basit alarmlar için Kibana arayüzü oldukça kullanışlı. Oluşturduğunuz Watch’ın JSON’ını da Kibana’dan kopyalayabilirsiniz, bu sayede API ile daha karmaşık watch’lar yazarken başlangıç noktası olarak kullanabilirsiniz.

Ancak şunu söylemeliyim: karmaşık chain input, transform veya ileri seviye Painless script gerektiren watch’lar için Kibana arayüzü yetersiz kalıyor. Ciddi işler için API ile çalışmak kaçınılmaz.

Sonuç

Elasticsearch Watcher, ELK Stack’i reaktif bir log görüntüleyiciden proaktif bir izleme platformuna dönüştürüyor. HTTP hata oranlarından disk kullanımına, anormal log hacminden servis sağlığına kadar kurumun ihtiyacına göre onlarca farklı alarm senaryosu kurabilirsiniz.

Başlamak için karmaşık senaryolara girmeyin. İlk hafta temel alarmları kurun: yüksek hata oranı, disk dolması, cluster sağlığı. Bunlar çalışır hale gelince throttle mekanizmalarını ve mesaj kalitesini iyileştirin. Sonra anormal durum tespiti gibi daha zekice kurallara geçin.

Watcher’ı gerçek anlamda faydalı kılan şey sadece alarm atması değil, doğru zamanda doğru kişiye doğru bilgiyi iletmesi. Bunu başardığınızda gece telefon açılma oranınızın düştüğünü göreceksiniz, ki bu her sysadmin’in hayalidir.

Bir yanıt yazın

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