Distributed Tracing: OpenTelemetry ile İzlenebilirlik
Modern mikroservis mimarilerinde, tek bir kullanıcı isteği onlarca servisten geçebilir. Bir istek yavaşladığında veya hata verdiğinde, sorunun hangi serviste, hangi metotta ve hangi veritabanı çağrısında oluştuğunu bulmak, log'lar arasında kaybolmak demektir. İşte Distributed Tracing (Dağıtık İzleme), bir isteğin tüm yolculuğunu tek bir görselleştirmede birleştirerek bu sorunu çözer. OpenTelemetry, bu izlemenin endüstri standardı haline gelmiş, sağlayıcıdan bağımsız açık kaynak bir çerçevedir.
1. Observability'nin Üç Sütunu ve Distributed Tracing'in Yeri
Observability (Gözlemlenebilirlik), üç temel sütundan oluşur:
| Sütun | Açıklama | Cevapladığı Soru |
|---|---|---|
| Logs (Günlükler) | Zaman damgalı olay kayıtları | "Ne oldu?" |
| Metrics (Metrikler) | Sayısal, toplanabilir veriler | "Ne kadar sık/sıcak?" |
| Traces (İzler) | Bir isteğin servisler arası yolculuğu | "Nerede ve neden yavaş?" |
Distributed Tracing, özellikle şu soruları yanıtlar:
-
Bir istek hangi servislerden geçti?
-
Her serviste ne kadar süre harcandı?
-
Hangi servis/veritabanı çağrısı darboğaz oluşturuyor?
-
Bir hata nerede başladı ve nasıl yayıldı?
2. Temel Kavramlar: Trace, Span ve Context
Trace (İz): Bir isteğin başlangıcından sonuna kadar tüm yolculuğunu temsil eder. Benzersiz bir Trace ID ile tanımlanır.
Span (Aralık): Trace içindeki her bir işlem birimidir. Bir HTTP çağrısı, bir veritabanı sorgusu veya bir metot çağrısı bir span olabilir. Her span şunları içerir:
-
Span ID: Benzersiz kimlik
-
Parent Span ID: Üst span'in kimliği (hiyerarşi)
-
Başlangıç ve Bitiş Zamanı: Süre hesaplaması
-
Attributes: Ek bilgiler (HTTP metodu, URL, durum kodu)
-
Events: Span içindeki önemli olaylar
-
Status: Başarılı, hatalı vb.
Context Propagation (Bağlam Yayılımı): Trace ID ve Span ID'nin servisler arasında taşınmasıdır. Bu sayede farklı servislerdeki span'ler aynı trace altında birleştirilir.
W3C Trace Context Standardı:
text
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
│ │ │ └─ Trace Flags
│ │ └─ Parent Span ID
│ └─ Trace ID (32 hex karakter)
└─ Version
Baggage: Trace boyunca taşınan key-value çiftleri (ör. tenant-id, user-id).
3. OpenTelemetry Nedir?
OpenTelemetry (OTel), CNCF (Cloud Native Computing Foundation) tarafından yönetilen, açık kaynaklı bir observability çerçevesidir. Log, metrik ve trace verilerini toplamak, işlemek ve dışa aktarmak için sağlayıcıdan bağımsız bir standart sunar.
OpenTelemetry'nin Avantajları:
-
Vendor-Agnostic: Jaeger, Zipkin, Datadog, Application Insights gibi farklı arka uçlarla çalışır. Arka uç değiştirmek için sadece exporter değiştirilir.
-
Standart: Tüm diller için tutarlı API ve SDK.
-
Otomatik Enstrümantasyon: ASP.NET Core, HttpClient, EF Core gibi popüler kütüphaneler için hazır enstrümantasyon.
-
Bağlam Yayılımı: W3C Trace Context standardını otomatik uygular.
OpenTelemetry Mimarisi:
text
[Uygulama] → [OTel SDK] → [Exporter] → [Collector] → [Backend (Jaeger, Zipkin, vb.)]
4. .NET'te OpenTelemetry Entegrasyonu
A. Gerekli Paketler
bash
dotnet add package OpenTelemetry.Extensions.Hosting dotnet add package OpenTelemetry.Instrumentation.AspNetCore dotnet add package OpenTelemetry.Instrumentation.Http dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol dotnet add package OpenTelemetry.Exporter.Jaeger
B. Temel Yapılandırma (Program.cs)
csharp
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(serviceName: "OrderService", serviceVersion: "1.0.0")
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName
}))
.WithTracing(tracing => tracing
// Otomatik enstrümantasyon
.AddAspNetCoreInstrumentation(options =>
{
options.RecordException = true;
options.Filter = httpContext =>
!httpContext.Request.Path.StartsWithSegments("/health");
})
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation(options =>
{
options.SetDbStatementForText = true;
})
// Manuel enstrümantasyon için kaynak
.AddSource("OrderService.*")
// Exporter
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://localhost:4317");
})
.AddJaegerExporter(options =>
{
options.AgentHost = "localhost";
options.AgentPort = 6831;
}));
var app = builder.Build();
app.Run();
C. Manuel Enstrümantasyon (Activity API)
csharp
using System.Diagnostics;
public class OrderService
{
// ActivitySource, manuel span oluşturmak için kullanılır
private static readonly ActivitySource ActivitySource = new("OrderService.Orders");
private readonly ILogger<OrderService> _logger;
public async Task<Order> CreateOrderAsync(CreateOrderCommand command)
{
// Yeni bir span başlat
using var activity = ActivitySource.StartActivity("CreateOrder", ActivityKind.Internal);
// Span'e özel bilgiler ekle (attribute)
activity?.SetTag("order.customer_id", command.CustomerId);
activity?.SetTag("order.total_amount", command.TotalAmount);
try
{
_logger.LogInformation("Sipariş oluşturuluyor: {CustomerId}", command.CustomerId);
var order = new Order { ... };
await _repository.SaveAsync(order);
// Başarılı olarak işaretle
activity?.SetStatus(ActivityStatusCode.Ok);
activity?.AddEvent(new ActivityEvent("OrderCreated",
tags: new ActivityTagsCollection { ["order.id"] = order.Id }));
return order;
}
catch (Exception ex)
{
// Hata durumunu kaydet
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
}
5. Context Propagation: Servisler Arası İzleme
A. Otomatik Yayılım (HttpClient ile)
AddHttpClientInstrumentation() eklendiğinde, HttpClient çağrılarında traceparent başlığı otomatik olarak eklenir. Karşı servis de aynı enstrümantasyona sahipse, span'ler otomatik olarak birleştirilir.
csharp
// Servis A: HttpClient çağrısı
var response = await _httpClient.GetAsync("http://orderservice/api/orders/123");
// traceparent başlığı otomatik eklenir
B. Manuel Yayılım (Message Queue için)
RabbitMQ, Kafka gibi mesaj kuyruklarında, trace context'i mesaj header'larına manuel eklemek gerekir.
csharp
using OpenTelemetry;
using OpenTelemetry.Context.Propagation;
private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator;
// Mesaj gönderirken context'i enjekte et
public void PublishMessage(string message)
{
var activity = Activity.Current;
var headers = new Dictionary<string, string>();
Propagator.Inject(new PropagationContext(activity?.Context ?? default, Baggage.Current),
headers, (carrier, key, value) => carrier[key] = value);
// Mesajı header'larla gönder
_rabbitMq.Publish(message, headers);
}
// Mesaj alırken context'i çıkar
public void ConsumeMessage(string message, Dictionary<string, string> headers)
{
var parentContext = Propagator.Extract(default, headers,
(carrier, key) => carrier.TryGetValue(key, out var value) ? new[] { value } : Array.Empty<string>());
Baggage.Current = parentContext.Baggage;
using var activity = ActivitySource.StartActivity("ProcessMessage",
ActivityKind.Consumer, parentContext.ActivityContext);
// Mesajı işle...
}
6. OpenTelemetry Collector: Merkezi Toplama
OpenTelemetry Collector, farklı kaynaklardan gelen telemetri verilerini toplayan, işleyen ve dışa aktaran bir aracıdır (agent). Uygulamalar doğrudan arka uca göndermek yerine Collector'a gönderir; Collector, veriyi filtreleyebilir, zenginleştirebilir ve birden fazla arka uca yönlendirebilir.
Collector Yapılandırması (otel-collector-config.yaml):
yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
attributes:
actions:
- key: environment
value: production
action: upsert
exporters:
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
prometheus:
endpoint: "0.0.0.0:8889"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, attributes]
exporters: [jaeger]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
7. Arka Uçlar: Jaeger, Zipkin ve Diğerleri
| Arka Uç | Açıklama | Özellikler |
|---|---|---|
| Jaeger | Uber tarafından geliştirilen, CNCF projesi | Zengin UI, bağımlılık grafiği, distributed context propagation |
| Zipkin | Twitter tarafından geliştirilen, olgun | Basit UI, hafif, geniş dil desteği |
| Grafana Tempo | Grafana ekosistemiyle entegre | Ucuz depolama, Grafana ile görselleştirme |
| Azure Application Insights | Microsoft'un bulut çözümü | Azure entegrasyonu, otomatik enstrümantasyon |
| Datadog APM | Ticari çözüm | Kapsamlı özellikler, kolay kurulum |
Docker Compose ile Jaeger:
yaml
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
environment:
- COLLECTOR_OTLP_ENABLED=true
8. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Sampling (Örnekleme) Kullanın | Yüksek trafikli sistemlerde tüm trace'leri toplamak pahalıdır. ParentBased(TraceIdRatioBased(0.1)) ile %10 örnekleme yapın. |
| Anlamlı Span İsimleri | GET /api/orders/{id} gibi anlamlı isimler kullanın; HTTP GET gibi genel isimlerden kaçının. |
| Attribute'ları Zenginleştirin | user.id, tenant.id, http.status_code gibi bağlam bilgileri ekleyin. |
| Hassas Veri Kaydetmeyin | Trace'lere PII (kişisel veri), şifreler, token'lar eklemeyin. |
| Health Check'leri Filtreleyin | /health, /metrics gibi endpoint'leri trace dışında bırakın. |
| Baggage'ı Dikkatli Kullanın | Baggage, tüm servisler arasında taşınır; boyutunu sınırlayın. |
| Collector Kullanın | Uygulamalar doğrudan arka uca bağlanmak yerine Collector'a gönderin. |
| Örnekleme Stratejisi Belirleyin | Head-based (basit) veya tail-based (akıllı) örnekleme arasında seçim yapın. |
| Korelasyon ID'lerini İlişkilendirin | Log'lardaki trace_id ve span_id ile trace'leri loglarla ilişkilendirin. |
9. Sampling (Örnekleme) Stratejileri
Yüksek trafikli sistemlerde, tüm trace'leri toplamak hem maliyetli hem de gereksizdir.
A. Head-Based Sampling (Başlangıçta Karar)
Karar, trace'in başlangıcında verilir. Basit ve hızlıdır ancak hatalı trace'leri kaçırabilir.
csharp
.WithTracing(tracing => tracing
.SetSampler(new TraceIdRatioBasedSampler(0.1)) // %10 örnekleme
)
B. Tail-Based Sampling (Sonunda Karar)
Karar, trace tamamlandıktan sonra verilir. Hatalı veya yavaş trace'ler her zaman örneklenir. OpenTelemetry Collector ile yapılır.
yaml
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: errors-policy
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-traces-policy
type: latency
latency: { threshold_ms: 1000 }
- name: random-policy
type: probabilistic
probabilistic: { sampling_percentage: 5 }
Sonuç
Distributed Tracing, mikroservis mimarisinde sorunları hızlıca tespit etmenin ve performansı optimize etmenin anahtarıdır. OpenTelemetry, sağlayıcıdan bağımsız, standart ve zengin otomatik enstrümantasyon desteğiyle bu alanda endüstri standardı haline gelmiştir. .NET ekosisteminde OpenTelemetry.Extensions.Hosting ve ilgili paketlerle ASP.NET Core, HttpClient, EF Core gibi bileşenler otomatik olarak enstrümante edilir; manuel enstrümantasyon için ActivitySource ve Activity API'leri kullanılır. W3C Trace Context standardı ile servisler arası bağlam yayılımı otomatik sağlanır. Jaeger, Zipkin veya Grafana Tempo gibi arka uçlarla görselleştirme yapılır. Sampling stratejileri ile maliyet kontrol altına alınır. Unutmayın: İyi bir izleme, tahmin etmeyi ortadan kaldırır; veriye dayalı karar vermenizi sağlar