Microservices'te Log Aggregation ve Correlation ID
Mikroservis mimarisinde, tek bir kullanıcı isteği onlarca servisten geçer ve her servis kendi log'unu kendi container'ına yazar. Bir sorun oluştuğunda, "hangi serviste ne oldu?" sorusunu yanıtlamak için onlarca farklı yere bakmak gerekir. İşte Log Aggregation (Merkezi Log Toplama) ve Correlation ID (Korelasyon Kimliği), bu kaosu düzene çeviren iki temel yapı taşıdır.
1. Neden Merkezi Log Toplama Gerekli?
Geleneksel (Monolitik) Yaklaşım:
text
Uygulama → app.log dosyası → Geliştirici okur
Mikroservis Yaklaşımı:
text
Order Service → Container log Payment Service → Container log Inventory Service → Container log Notification Service → Container log ... (50+ servis)
Sorunlar:
-
Dağıtık Log'lar: Her servisin log'u ayrı yerde; korelasyon yok.
-
Ephemeral Container'lar: Container silinince log'lar da kaybolur.
-
Ölçeklenme: 100 pod'un log'unu manuel toplamak imkansız.
-
Arama Zorluğu: "Sipariş 123 için ne oldu?" sorusuna tek noktadan yanıt yok.
Çözüm: Merkezi log toplama + Correlation ID.
2. Correlation ID: Dağıtık İzlemenin Anahtarı
Correlation ID, bir kullanıcı isteğinin tüm servisler arasındaki yolculuğunu takip eden benzersiz bir tanımlayıcıdır. Her servis, gelen Correlation ID'yi log'una ekler ve downstream servislere iletir.
Temel Akış:
text
Client → API Gateway
(Correlation ID üretir: abc-123)
↓
Order Service
(Log: [abc-123] Sipariş alındı)
↓ (HTTP header: X-Correlation-ID: abc-123)
Payment Service
(Log: [abc-123] Ödeme işlendi)
↓ (Message header: CorrelationId=abc-123)
Notification Service
(Log: [abc-123] E-posta gönderildi)
Sonuç: abc-123 araması ile tüm servislerdeki ilgili log'lar tek ekranda görünür.
3. Correlation ID Yayılımı (Propagation)
Correlation ID'nin tüm servisler ve iletişim kanalları boyunca taşınması gerekir.
A. HTTP Çağrılarında Yayılım
csharp
// Gelen istekten Correlation ID al veya üret
public class CorrelationIdMiddleware
{
private const string HeaderName = "X-Correlation-ID";
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext context)
{
// 1. Gelen header'dan al veya yeni üret
var correlationId = context.Request.Headers[HeaderName].FirstOrDefault()
?? Guid.NewGuid().ToString();
// 2. HttpContext.Items'a ekle (request boyunca erişilebilir)
context.Items["CorrelationId"] = correlationId;
// 3. Yanıt header'ına ekle (istemci de görsün)
context.Response.OnStarting(() =>
{
context.Response.Headers[HeaderName] = correlationId;
return Task.CompletedTask;
});
// 4. Log context'ine ekle (Serilog)
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await _next(context);
}
}
}
B. HttpClient ile Otomatik Yayılım
csharp
// DelegatingHandler - giden isteklere Correlation ID ekler
public class CorrelationIdDelegatingHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var correlationId = _httpContextAccessor.HttpContext?.Items["CorrelationId"]?.ToString()
?? Guid.NewGuid().ToString();
request.Headers.Add("X-Correlation-ID", correlationId);
return await base.SendAsync(request, cancellationToken);
}
}
// Kayıt
builder.Services.AddTransient<CorrelationIdDelegatingHandler>();
builder.Services.AddHttpClient("PaymentService")
.AddHttpMessageHandler<CorrelationIdDelegatingHandler>();
C. Message Queue ile Yayılım (MassTransit)
csharp
// Publish sırasında Correlation ID ekle
public class CorrelationIdPublishFilter<T> : IFilter<PublishContext<T>>
where T : class
{
private readonly IHttpContextAccessor _httpContextAccessor;
public async Task Send(PublishContext<T> context, IPipe<PublishContext<T>> next)
{
var correlationId = _httpContextAccessor.HttpContext?.Items["CorrelationId"]?.ToString();
if (correlationId != null)
context.Headers.Set("X-Correlation-ID", correlationId);
await next.Send(context);
}
}
// Consume sırasında Correlation ID'yi geri yükle
public class CorrelationIdConsumeFilter<T> : IFilter<ConsumeContext<T>>
where T : class
{
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
var correlationId = context.Headers.Get<string>("X-Correlation-ID")
?? Guid.NewGuid().ToString();
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await next.Send(context);
}
}
}
D. Background Service'lerde Yayılım
csharp
public class OrderProcessor : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var message = await _queue.DequeueAsync(stoppingToken);
// Her mesaj için yeni bir log context
using (LogContext.PushProperty("CorrelationId", message.CorrelationId))
{
_logger.LogInformation("Mesaj işleniyor: {MessageId}", message.Id);
await ProcessAsync(message);
}
}
}
}
4. Yapılandırılmış Loglama (Structured Logging)
Merkezi log toplamada, log'ların JSON formatında ve aranabilir alanlar içermesi kritiktir. Serilog, .NET ekosisteminde en yaygın kullanılan kütüphanedir.
Serilog Konfigürasyonu:
csharp
// Program.cs
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext() // CorrelationId gibi alanlar
.Enrich.WithProperty("ServiceName", "OrderService")
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
.Enrich.WithMachineName()
.WriteTo.Console(new JsonFormatter()) // JSON formatı
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200"))
{
AutoRegisterTemplate = true,
IndexFormat = "logs-{0:yyyy.MM.dd}",
NumberOfShards = 2,
NumberOfReplicas = 1
})
.CreateLogger();
builder.Host.UseSerilog();
Yapılandırılmış Log Örneği:
json
{
"@timestamp": "2026-10-15T10:30:45.123Z",
"level": "Information",
"messageTemplate": "Sipariş oluşturuldu: {OrderId}",
"properties": {
"OrderId": 123,
"CorrelationId": "abc-123-def",
"ServiceName": "OrderService",
"Environment": "Production",
"MachineName": "order-service-7d9f8b-x2k4m"
},
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7"
}
Bu sayede Elasticsearch'te CorrelationId: "abc-123-def" araması ile tüm ilgili log'lar bulunur.
5. Log Aggregation Stack'leri
A. ELK Stack (Elasticsearch, Logstash, Kibana)
text
Uygulama → Logstash → Elasticsearch → Kibana
-
Elasticsearch: Log depolama ve arama motoru
-
Logstash: Log toplama ve dönüştürme
-
Kibana: Görselleştirme ve arama arayüzü
B. EFK Stack (Elasticsearch, Fluentd/Fluent Bit, Kibana)
text
Uygulama → Fluent Bit (DaemonSet) → Elasticsearch → Kibana
-
Fluent Bit: Hafif, düşük kaynak tüketimi; Kubernetes'te DaemonSet olarak çalışır
-
Fluentd: Daha zengin plugin ekosistemi
C. Grafana Loki (Modern Alternatif)
text
Uygulama → Promtail → Loki → Grafana
-
Loki: Elasticsearch'e göre daha ucuz, sadece metadata indeksler
-
Promtail: Log toplama agent'ı
-
Grafana: Görselleştirme (metrics ve logs bir arada)
D. Cloud Çözümleri
| Bulut | Servis |
|---|---|
| Azure | Azure Monitor Logs / Application Insights |
| AWS | CloudWatch Logs |
| GCP | Cloud Logging |
6. Kubernetes'te Log Toplama Mimarisi
text
┌─────────────────────────────────────────┐
│ Pod: order-service │
│ ┌────────────┐ ┌────────────┐ │
│ │ App │ │ Fluent Bit │ │
│ │ (stdout) │─>│ (sidecar) │ │
│ └────────────┘ └────────────┘ │
└─────────────────────────────────────────┘
│
↓
┌─────────────────────┐
│ Fluent Bit │
│ (DaemonSet) │
└─────────────────────┘
│
↓
┌─────────────────────┐
│ Elasticsearch │
└─────────────────────┘
│
↓
┌─────────────────────┐
│ Kibana │
└─────────────────────┘
Kritik Nokta: Uygulama log'ları stdout'a yazmalıdır (12-Factor App prensibi). Kubernetes, stdout'u otomatik olarak toplar; Fluent Bit DaemonSet'i tüm node'lardan log'ları Elasticsearch'e gönderir.
7. TraceId ile Correlation ID Entegrasyonu
Modern observability'de, Correlation ID yerine OpenTelemetry TraceId kullanmak daha güçlüdür. TraceId, hem log'larda hem trace'lerde aynıdır; log-trace korelasyonu sağlar.
Serilog + OpenTelemetry Entegrasyonu:
csharp
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
);
// Serilog'a TraceId ve SpanId ekle
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With<ActivityEnricher>() // TraceId, SpanId ekler
.WriteTo.Console(new JsonFormatter())
.CreateLogger();
Özel ActivityEnricher:
csharp
public class ActivityEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var activity = Activity.Current;
if (activity != null)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
"TraceId", activity.TraceId.ToString()));
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
"SpanId", activity.SpanId.ToString()));
}
}
}
Sonuç: Kibana'da bir log bulduğunuzda, aynı TraceId ile Jaeger'da ilgili trace'e geçebilirsiniz.
8. Gerçek Dünya Senaryosu: Sipariş Hatası
Senaryo: Kullanıcı "Sipariş 12345 oluşturulamadı" hatası alıyor. Hangi serviste sorun var?
Adım 1: Kibana'da Ara
text
CorrelationId: "abc-123-def" OR OrderId: 12345
Adım 2: Log'ları İncele
text
[abc-123-def] OrderService: Sipariş oluşturuluyor... [abc-123-def] OrderService: PaymentService çağrılıyor... [abc-123-def] PaymentService: Ödeme işleniyor... [abc-123-def] PaymentService: ERROR - Kart reddedildi (Kod: 51) [abc-123-def] OrderService: ERROR - Ödeme başarısız, sipariş iptal edildi
Adım 3: Kök Nedeni Bul
Ödeme servisi, kart reddi (yetersiz bakiye) nedeniyle başarısız olmuş.
Adım 4: Trace ile Doğrula
TraceId: 4bf92f3577b34da6a3ce929d0e0e4736 ile Jaeger'da trace'i aç; PaymentService span'inde hata detayını gör.
Sonuç: 3 farklı servisin log'u tek bir arama ile ilişkilendirildi; sorun 2 dakikada tespit edildi.
9. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Correlation ID'yi Gateway'de Üret | İlk giriş noktası (API Gateway) üretmeli; downstream servisler aynısını kullanmalı. |
| Yapılandırılmış Log Kullan | JSON formatı; aranabilir alanlar (CorrelationId, OrderId, UserId). |
| stdout'a Log Yaz | 12-Factor App prensibi; container ortamında dosya yazma. |
| Hassas Veri Loglama | Şifre, token, kredi kartı gibi bilgileri log'lamayın. |
| TraceId ile İlişkilendir | Log'larda TraceId ve SpanId olsun; log-trace korelasyonu sağla. |
| Log Seviyelerini Yönet | Production'da Information ve üstü; Debug sadece gerektiğinde. |
| Retention Policy Belirle | Log'ları sonsuza kadar saklamayın; 30-90 gün tipik. |
| Index Yönetimi | Elasticsearch'te günlük index (logs-2026.10.15); ILM ile otomatik silme. |
| Sampling Kullan | Yüksek trafikte her log'u değil, önemli olanları topla. |
| Alerting Kur | ERROR seviyesindeki log'lar için otomatik uyarı. |
10. Log Aggregation vs. Distributed Tracing
| Özellik | Log Aggregation | Distributed Tracing |
|---|---|---|
| Amaç | Olayları kaydetmek ve aramak | İstek yolculuğunu görselleştirmek |
| Veri Türü | Metin/JSON log'lar | Span'ler ve süreler |
| Cevapladığı Soru | "Ne oldu?" | "Nerede yavaş?" |
| Korelasyon | CorrelationId | TraceId |
| Depolama Maliyeti | Yüksek (tüm log'lar) | Orta (örnekleme) |
| Kullanım | Hata ayıklama, audit | Performans analizi |
En İyi Yaklaşım: İkisini birlikte kullanın. Log'lar detay verir; trace'ler bağlam verir. TraceId ile her ikisini ilişkilendirin.
Sonuç
Mikroservis mimarisinde, log'ları merkezi olarak toplamak ve Correlation ID ile ilişkilendirmek, sorun tespitini saatlerden dakikalara indirir. Yapılandırılmış loglama (Serilog), ELK/EFK/Loki stack'leri, Kubernetes Fluent Bit DaemonSet'i ve OpenTelemetry TraceId entegrasyonu ile uçtan uca gözlemlenebilirlik sağlarsınız. Correlation ID'yi HTTP header'ları, mesaj kuyrukları ve background service'ler üzerinden yayarak, bir kullanıcı isteğinin tüm servislerdeki izini sürebilirsiniz. Unutmayın: Dağıtık sistemlerde, log'ları olmayan bir sorun, hiç yaşanmamış gibidir. Merkezi log toplama ve Correlation ID, bu sorunu çözmenin temelidir.