ELK Stack ile Mikro Servis Log Yönetimi
Mikro servis mimarisine geçtiğinizde ilk haftalarda her şey harika görünür. Servisler bağımsız deploy ediliyor, takımlar birbirinden bağımsız çalışıyor, ölçeklendirme kolaylaşıyor. Sonra production’da bir hata çıkıyor ve kendinizi 15 farklı servisin loguna bakarken buluyorsunuz. Kim neyi ne zaman yaptı, hangi servis hangi servisi çağırdı, hata nerede başladı? İşte bu noktada merkezi log yönetimi olmadan mikro servis mimarisi tam bir işkenceye dönüşüyor.
ELK Stack (Elasticsearch, Logstash, Kibana) bu sorunun en yaygın ve en olgun çözümlerinden biri. Beats ailesiyle birlikte ELKB veya Elastic Stack olarak da anılıyor artık. Bu yazıda sıfırdan kurulum değil, gerçekten production ortamında çalışan, ölçeklenebilir bir mikro servis log altyapısını nasıl kurarsınız onu anlatacağım. Elimden geldiğince “hello world” örneklerinden uzak durmaya çalışacağım.
Mimariyi Anlamak
Mikro servis ortamında log yönetimi için önce veri akışını kafanızda netleştirmeniz gerekiyor. Loglar servisten çıkıyor, bir şekilde toplanıyor, işleniyor ve sorgulanabilir hale geliyor. Bu akışta her adım kritik.
Bizim tercih ettiğimiz mimari şu şekilde işliyor:
- Uygulama servisleri –> JSON formatında stdout’a log yazıyor
- Filebeat –> Her node’da çalışıyor, container loglarını topluyor
- Logstash –> Parse, enrich, filter işlemleri burada yapılıyor
- Elasticsearch –> Indexleme ve saklama
- Kibana –> Görselleştirme ve arama
Neden Logstash var diye sorabilirsiniz, Filebeat direkt Elasticsearch’e yazamaz mı? Yazabilir, ancak production’da log hacmi arttığında ve log işleme mantığı karmaşıklaştığında Logstash’in buffer ve filter kabiliyetlerine ihtiyaç duyuyorsunuz. Filebeat’i bir ön tampon olarak da kullanabilirsiniz, Logstash düşse bile loglar kaybolmuyor.
Elasticsearch Cluster Kurulumu
Tek node Elasticsearch ile başlamayın. Kibana’da güzel dashboardlar görmek için başlangıçta tek node yeterli görünse de production’da minimum 3 node’luk bir cluster şart. Hem yüksek erişilebilirlik hem de shard dağılımı için.
docker-compose.yml ile başlayalım:
version: '3.8'
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- node.name=es01
- cluster.name=microservice-logs
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es01_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- elk
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- node.name=es02
- cluster.name=microservice-logs
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es02_data:/usr/share/elasticsearch/data
networks:
- elk
volumes:
es01_data:
es02_data:
networks:
elk:
driver: bridge
ES_JAVA_OPTS değerini sunucunuzun RAM’inin yarısı olarak ayarlayın, ama 30GB’ı geçmeyin. Elasticsearch’in kendine özel heap yönetimi var ve fazla vermek bazen daha kötü performansa yol açıyor.
Index Lifecycle Management (ILM) – Gözden Kaçırmayın
Bu kısmı atlayan sysadmin’lerin disk dolduğunda panikle beni aradığını defalarca gördüm. ILM, Elasticsearch’in en değerli özelliklerinden biri. Log indexlerini hot, warm, cold aşamalarından geçirip sonunda siliyor.
curl -X PUT "http://elasticsearch:9200/_ilm/policy/microservice-logs-policy"
-H "Content-Type: application/json"
-d '{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d"
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "2d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
},
"set_priority": {
"priority": 50
}
}
},
"cold": {
"min_age": "7d",
"actions": {
"set_priority": {
"priority": 0
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}'
Bu policy ile: loglar ilk gün hot’ta tutuluyor (hızlı SSD’ler burada olmalı), 2 gün sonra warm’a geçiyor ve sıkıştırılıyor, 7 gün sonra cold’a düşüyor, 30 gün sonra siliniyor. Compliance gereksinimleri varsa bu süreleri uzatın tabii.
Logstash Pipeline Tasarımı
Mikro servis ortamının asıl zorluğu, her servisin farklı log formatında yazmasıdır. Bir servis Python ile yazılmış, diğeri Go, diğeri Node.js. Hepsinin log formatı farklı. Logstash bu karmaşıklığı tek bir yerde çözmenizi sağlıyor.
# /etc/logstash/conf.d/microservices.conf
input {
beats {
port => 5044
ssl => false
}
}
filter {
# Container meta bilgilerini parse et
if [container][name] {
mutate {
add_field => { "service_name" => "%{[container][name]}" }
}
}
# JSON log parse et
if [message] =~ /^{/ {
json {
source => "message"
target => "parsed"
}
mutate {
rename => { "[parsed][level]" => "log_level" }
rename => { "[parsed][msg]" => "log_message" }
rename => { "[parsed][trace_id]" => "trace_id" }
rename => { "[parsed][user_id]" => "user_id" }
rename => { "[parsed][duration_ms]" => "duration_ms" }
}
}
# Log level normalizasyonu
# Farklı servisler "ERROR", "error", "ERR" yazabilir
if [log_level] {
mutate {
uppercase => [ "log_level" ]
}
}
# Yavaş sorgular için özel flag
if [duration_ms] and [duration_ms] > 1000 {
mutate {
add_tag => [ "slow_request" ]
add_field => { "alert_type" => "slow_request" }
}
}
# Timestamp düzeltme
date {
match => [ "[parsed][timestamp]", "ISO8601" ]
target => "@timestamp"
}
# Gereksiz alanları temizle
mutate {
remove_field => [ "parsed", "agent", "ecs" ]
}
}
output {
elasticsearch {
hosts => ["es01:9200", "es02:9200", "es03:9200"]
index => "microservices-%{service_name}-%{+YYYY.MM.dd}"
user => "elastic"
password => "${ES_PASSWORD}"
}
# Error logları ayrı bir indexe de yaz
if [log_level] == "ERROR" {
elasticsearch {
hosts => ["es01:9200", "es02:9200", "es03:9200"]
index => "microservices-errors-%{+YYYY.MM.dd}"
user => "elastic"
password => "${ES_PASSWORD}"
}
}
}
Bu pipeline’da dikkat etmenizi istediğim birkaç nokta var. Her servise özel ayrı index kullanıyoruz. Bu, hem ILM policy’lerini servis bazlı uygulayabilmemizi sağlıyor hem de Kibana’da izole analiz yapabilmemizi. Error loglarını ayrı bir indexe de yazıyoruz çünkü nöbetçi ekip için “sadece hatalara bak” şeklinde bir Kibana dashboard’u hazırlamak çok daha kolay hale geliyor.
Filebeat Konfigürasyonu
Filebeat, her Kubernetes worker node’unda veya Docker host’unda çalışıyor ve container loglarını toplayıp Logstash’e gönderiyor.
# filebeat.yml
filebeat.autodiscover:
providers:
- type: docker
hints.enabled: true
templates:
- condition:
contains:
docker.container.labels.collect_logs: "true"
config:
- type: container
paths:
- /var/lib/docker/containers/${data.docker.container.id}/*.log
json.message_key: log
json.keys_under_root: true
processors:
- add_docker_metadata:
host: "unix:///var/run/docker.sock"
- add_fields:
target: ''
fields:
environment: "${ENVIRONMENT:production}"
processors:
- drop_fields:
fields: ["host.architecture", "host.os", "agent.ephemeral_id"]
ignore_missing: true
output.logstash:
hosts: ["logstash:5044"]
loadbalance: true
bulk_max_size: 2048
queue.mem:
events: 4096
flush.min_events: 512
flush.timeout: 5s
logging.level: warning
logging.to_files: true
logging.files:
path: /var/log/filebeat
name: filebeat
keepfiles: 7
Docker container’larınıza collect_logs: "true" label’ı eklediğinizde otomatik olarak log toplama başlıyor. Bu önemli çünkü bazı container’lardan log toplamak istemeyebilirsiniz (mesela load test sırasında çalışan geçici container’lar gibi).
Uygulama Tarafı: Yapılandırılmış Loglama
Merkezi log sisteminin değeri, uygulamaların düzgün log yazmasıyla doğru orantılı. Eğer servisleriniz "hata oluştu" gibi açıklamasız string’ler yazıyorsa en iyi ELK kurulumu bile size çok şey kazandırmaz.
Node.js için örnek bir logger konfigürasyonu:
# logger.js - Winston ile yapılandırılmış loglama
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: process.env.SERVICE_NAME || 'unknown-service',
version: process.env.APP_VERSION || '0.0.0',
environment: process.env.NODE_ENV || 'development'
},
transports: [
new winston.transports.Console()
]
});
// Request middleware
function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('http_request', {
method: req.method,
path: req.path,
status_code: res.statusCode,
duration_ms: duration,
trace_id: req.headers['x-trace-id'],
user_id: req.user?.id,
ip: req.ip
});
});
next();
}
module.exports = { logger, requestLogger };
Burada trace_id alanı kritik. Distributed tracing olmadan mikro servislerde bir isteğin hangi servislerden geçtiğini takip etmek neredeyse imkansız. Her servis bu trace ID’yi loglarına yazmalı ve downstream servislere header olarak iletmeli.
Kibana’da Anlamlı Dashboard’lar Kurmak
Kibana’yı açıp “tüm loglar” görünümünde gezinmek size çok az şey söyler. Asıl değer, iş sorularını yanıtlayan dashboard’lardan geliyor.
Index pattern oluşturduktan sonra şu Kibana saved search’leri mutlaka hazırlayın:
# Kibana DevTools ile hızlı analiz sorguları
# Son 1 saatin error dağılımı servise göre
GET microservices-*/_search
{
"size": 0,
"query": {
"bool": {
"filter": [
{ "term": { "log_level": "ERROR" } },
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
},
"aggs": {
"errors_by_service": {
"terms": {
"field": "service_name.keyword",
"size": 20
}
}
}
}
# Yavaş request'leri bul (1 saniyenin üzeri)
GET microservices-*/_search
{
"size": 20,
"query": {
"bool": {
"filter": [
{ "range": { "duration_ms": { "gte": 1000 } } },
{ "range": { "@timestamp": { "gte": "now-24h" } } }
]
}
},
"sort": [
{ "duration_ms": { "order": "desc" } }
],
"_source": ["service_name", "path", "duration_ms", "trace_id", "@timestamp"]
}
Alerting: Kibana Watcher ile Proaktif İzleme
Log toplamak güzel ama bir şeyler patladığında sizi kim uyaracak? Kibana Watcher ile Elasticsearch üzerinde periyodik sorgular çalıştırıp Slack’e veya e-postaya alert gönderebilirsiniz.
PUT _watcher/watch/error-rate-spike
{
"trigger": {
"schedule": {
"interval": "5m"
}
},
"input": {
"search": {
"request": {
"indices": ["microservices-errors-*"],
"body": {
"query": {
"range": {
"@timestamp": {
"gte": "now-5m"
}
}
},
"aggs": {
"error_count": {
"value_count": {
"field": "_id"
}
}
}
}
}
}
},
"condition": {
"compare": {
"ctx.payload.aggregations.error_count.value": {
"gt": 50
}
}
},
"actions": {
"notify_slack": {
"webhook": {
"scheme": "https",
"host": "hooks.slack.com",
"port": 443,
"method": "post",
"path": "/services/YOUR_WEBHOOK_PATH",
"body": "{"text": "UYARI: Son 5 dakikada {{ctx.payload.aggregations.error_count.value}} hata tespit edildi!"}"
}
}
}
}
Bu watcher 5 dakikada bir çalışıyor ve son 5 dakikada 50’den fazla error log görürse Slack’e mesaj atıyor. Threshold değerini servisinizin normal hata oranına göre ayarlayın.
Performans ve Kapasite Planlaması
Production’da karşılaştığım en büyük sorunlardan biri yetersiz kapasite planlaması. Birkaç pratik kural:
Elasticsearch shard boyutu: Bir shard 20-40GB arasında tutmaya çalışın. Çok küçük shard’lar overhead yaratır, çok büyük shard’lar recovery’yi uzatır.
Logstash batch size: Varsayılan 125’tir, yüksek hacimli ortamlarda 500-1000’e çıkarabilirsiniz.
Disk hesabı: Günlük log hacminizi ölçün ve ILM retention sürenizle çarpın, üzerine %30 buffer ekleyin. 10GB/gün log üretiyorsanız ve 30 gün saklayacaksanız minimum 390GB disk planlamanız gerekiyor.
JVM heap: Elasticsearch node başına 30GB’ı geçmeyin, bu Elasticsearch’ün kendi önerisi. Bunun yerine daha fazla node ekleyin.
# Elasticsearch cluster sağlık kontrolü scripti
#!/bin/bash
ES_HOST="http://localhost:9200"
ES_USER="elastic"
ES_PASS="your_password"
echo "=== Cluster Health ==="
curl -s -u $ES_USER:$ES_PASS "$ES_HOST/_cluster/health?pretty"
echo ""
echo "=== Index Boyutları ==="
curl -s -u $ES_USER:$ES_PASS "$ES_HOST/_cat/indices/microservices-*?v&h=index,store.size,docs.count&s=store.size:desc" | head -20
echo ""
echo "=== Node Disk Kullanimi ==="
curl -s -u $ES_USER:$ES_PASS "$ES_HOST/_cat/allocation?v&h=node,disk.used,disk.avail,disk.percent"
Bu scripti cron’a ekleyin ve çıktısını bir monitoring sistemine gönderin. Disk dolmadan önce alarm almanız kritik.
Gerçek Dünya Senaryosu: Production Krizi
Geçen ay bir e-ticaret müşterimizde tam kampanya günü checkout servisi yavaşlamaya başladı. Eski yaklaşımla 30 dakika harcardık log dosyaları arasında gezinmeye. ELK ile ne yaptık?
Önce Kibana’da son 30 dakikanın error oranı grafiğine baktık, checkout servisinde spike görüldü. Trace ID’yi alıp tüm servisler genelinde filtreledik. Payment Gateway servisinin response time’ının 200ms’den 8 saniyeye çıktığını gördük. Logstash’teki duration_ms > 1000 tagımız sayesinde bu loglar zaten slow_request etiketiyle işaretlenmişti. Arama saniyeler içinde oldu.
Sorunun kaynağı Payment Gateway’in kullandığı bir external API’nin timeout vermesiydi. ELK olmadan bu tespiti yapmak için 4-5 farklı servisi manuel incelememiz gerekirdi.
Sonuç
ELK Stack kurulu olması sizi kurtarmaz, doğru kurulu olması kurtarır. Bu yazıda anlattıklarımın özeti şu şekilde:
- Uygulamalar structured JSON log yazmalı, trace_id şart
- ILM’i baştan kurun, sonradan disk krizi yaşamayın
- Logstash’te normalizasyon yapın, her servisin farklı formatını merkezi olarak çözün
- Servise özel ayrı indexler kullanın
- Sadece log toplamayın, Watcher ile proaktif alert kurun
- Kapasite planlamasını ihmal etmeyin
Mikro servis mimarisine geçiş büyük bir adım, ama merkezi log altyapısı olmadan bu geçiş yarım kalır. ELK Stack olgun, topluluk desteği güçlü bir çözüm. Elastic’in cloud versiyonu (Elastic Cloud) ile başlayıp sonradan self-hosted’a geçebilirsiniz, ya da tam tersini yapabilirsiniz. Önemli olan log verilerinizin bir yerinde toplanmış, aranabilir ve analiz edilebilir olması.
Production’da log yönetimi bir lüks değil, operasyonel zorunluluk. Bunu ne kadar erken anlarsanız o kadar az gecenizi troubleshooting’de geçirirsiniz.
