.NET Core
.NET'te Logging ve Monitoring: Serilog ve Application Insights
Production ortamında .NET sistemleri yonetmek icin observability olmazsa olmazdır. Tek basına log tutmak yetmez; loglar, metrikler ve trace'ler birbiriyle baglantılı, tutarlı bir resim olusturmalıdır. Production sistemlerde gozlemledigim kadariyla, yapılandırılmıs observability altyapısı olmayan ekipler, sorunları cozmekte 3-5 kat daha fazla zaman harcıyor.
Bu yazıda Serilog, Application Insights ve OpenTelemetry kullanarak saglam bir observability temeli kurmayı ele alacagım.
Serilog ile Structured Logging
Structured Logging Neden Onemli?
"42 numaralı kullanıcı 1001 numaralı siparisi olusturdu" gibi duz metin loglar insan icin okunabilir olsa da makine tarafından islenemez. Saniyede binlerce istek gecen birden fazla serviste, loglarınızın aranabilir, filtrelenebilir ve toplanabilir olması sart.
Structured logging, mesajın yanında verileri key-value olarak yakalar. Boylece loglarınızı bir veritabanı gibi sorgulayabilirsiniz.
Serilog Kurulumu
Production ortamları icin kullandıgım temel Serilog yapılandırması:
using Serilog;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("ServiceName", "OrderApi")
.WriteTo.Console(theme: AnsiConsoleTheme.Code)
.WriteTo.ApplicationInsights(
telemetryConfiguration,
TelemetryConverter.Traces)
.WriteTo.Async(a => a.File(
path: "logs/order-api-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 14,
fileSizeLimitBytes: 100_000_000))
.CreateLogger();Program.cs icerisinde kayıt:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
var app = builder.Build();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
};
});Message Template Kullanımı
Serilog, string interpolation yerine message template kullanır. Bu ayrım cok onemlidir:
// YANLIS - string interpolation yapıyı bozar
_logger.LogInformation($"Siparis {orderId} kullanıcı {userId} tarafından olusturuldu");
// DOGRU - message template adlandırılmıs ozellikleri korur
_logger.LogInformation("Siparis {OrderId} kullanıcı {UserId} tarafından olusturuldu", orderId, userId);
// DOGRU - karmasık nesneler icin @ ile destructure edin
_logger.LogInformation("Siparis olusturuldu: {@SiparisDetay}", new { orderId, userId, total, itemCount });Dogru yontemle daha sonra log toplayıcınızda WHERE OrderId = 1001 seklinde sorgulama yapabilirsiniz. String interpolation kullandıgınızda bu ozellik metin icinde kaybolur.
Enricher'lar ve Contextual Logging
Enricher'lar, bir kapsam icerisindeki her log kaydına otomatik olarak ozellik ekler. Bir istegin yasam dongusu boyunca izlenmesi icin cok degerlidir:
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Response.Headers["X-Correlation-ID"] = correlationId;
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await _next(context);
}
}
}Production sistemlerde gozlemledigim kadariyla, correlation ID olmadan servisler arası sorunları debug etmek tamamen tahmine donusur. API gateway'den baslayıp her alt servis cagrisina kadar akan tek bir correlation ID, yapabileceginiz en etkili observability yatırımıdır.
Ozel Enricher Yazımı
Tekrarlanan context bilgisi icin yeniden kullanılabilir enricher olusturun:
public class TenantEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantEnricher(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var tenantId = _httpContextAccessor.HttpContext?.Items["TenantId"]?.ToString();
if (!string.IsNullOrEmpty(tenantId))
{
logEvent.AddPropertyIfAbsent(
propertyFactory.CreateProperty("TenantId", tenantId));
}
}
}Structured Logging En Iyi Uygulamaları
Dogru Log Seviyesi Kullanımı
Her seviyenin bir amacı vardır. Yanlıs kullanım ya gurultu yaratır ya da kritik bilgiyi gizler:
- Verbose/Trace: Framework'un ic detayları. Production'da neredeyse hic acılmaz.
- Debug: Gelistirme sırasında yararlı teshis bilgisi. Sorun giderirken servis bazlı acın.
- Information: Is acısından onemli olaylar. "Siparis olusturuldu", "Odeme islendi", "Kullanıcı giris yaptı".
- Warning: Beklenmedik bir sey oldu ama sistem kendini topladı. Yeniden deneme basarılı, cache miss.
- Error: Bir islem basarısız oldu. Istek tamamlanamadı ama servis calısmaya devam ediyor.
- Fatal/Critical: Surec cokmek uzere. Kurtarılamaz durum, bellek yetersizligi, bozuk yapılandırma.
// Iyi: Information seviyesinde net is olayı
_logger.LogInformation("Odeme {PaymentId} siparis {OrderId} icin islendi, tutar {Amount:C}",
paymentId, orderId, amount);
// Iyi: kurtarılabilir sorun icin uyarı
_logger.LogWarning("Urun {ProductId} icin cache bos, veritabanına donuluyor", productId);
// Iyi: tam exception context'i ile hata
_logger.LogError(ex, "Odeme {PaymentId} siparis {OrderId} icin islenemedi", paymentId, orderId);Log Hacmini Kontrol Altında Tutma
Production sistemlerde gozlemledigim kadariyla, bazı servisler gunde 50GB'den fazla log uretiyordu; cunku gelisitriciler her dongu adımını Information seviyesinde loglamıstı. Yuksek log hacmi para yer, sorguları yavaslatır ve sinyali gurultude gozer.
Temel kurallar:
- Is operasyonlarının giris/cıkısını loglayın, her metod cagrisını degil
- Sadece aktif inceleme sırasında ihtiyac duyacaklarınız icin Debug seviyesini kullanın
- Sıkı donguler icinde loglama yapmayın; bunun yerine toplayıp ozet loglayın
- Gurultulu framework namespace'leri icin minimum seviye override'ları ayarlayın
Tutarlı Ozellik Adlandırması
Ekip genelinde adlandırma kuralları belirleyin ve uygulayın:
// Tutarsız - sorgulamak kabus
_logger.LogInformation("Kullanıcı {user_id} icin islendi", userId);
_logger.LogInformation("Kullanıcı {UserId} icin islendi", userId);
_logger.LogInformation("Kullanıcı {uid} icin islendi", userId);
// Tutarlı - bir kural secin (PascalCase onerilir) ve sadık kalın
_logger.LogInformation("Kullanıcı {UserId} icin islendi", userId);Application Insights Entegrasyonu
Istek ve Bagımlılık Takibi
Application Insights, HTTP istekleri, SQL cagriları ve dıs bagımlılıklar icin otomatik telemetri saglar:
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
options.EnableAdaptiveSampling = true;
});
// Is metrikleri icin ozel telemetri
public class OrderService
{
private readonly TelemetryClient _telemetry;
public OrderService(TelemetryClient telemetry)
{
_telemetry = telemetry;
}
public async Task<Order> PlaceOrderAsync(OrderRequest request)
{
var stopwatch = Stopwatch.StartNew();
try
{
var order = await ProcessOrder(request);
_telemetry.TrackEvent("OrderPlaced", new Dictionary<string, string>
{
["OrderId"] = order.Id.ToString(),
["ItemCount"] = request.Items.Count.ToString()
}, new Dictionary<string, double>
{
["OrderTotal"] = (double)order.Total,
["ProcessingTimeMs"] = stopwatch.ElapsedMilliseconds
});
return order;
}
catch (Exception ex)
{
_telemetry.TrackException(ex, new Dictionary<string, string>
{
["Operation"] = "PlaceOrder",
["UserId"] = request.UserId.ToString()
});
throw;
}
}
}Adaptive Sampling
Yuksek trafik hacimlerinde her trace'i Application Insights'a gondermek pahali ve gereksizdir. Adaptive sampling, hata gorununurlugunu korurken maliyetleri yonetilebilir tutar:
builder.Services.Configure<TelemetryConfiguration>(config =>
{
var processor = config.DefaultTelemetrySink.TelemetryProcessorChainBuilder;
processor.UseAdaptiveSampling(
maxTelemetryItemsPerSecond: 5,
excludedTypes: "Exception;Event");
processor.Build();
});Kilit nokta: Exception ve kritik Event'leri her zaman sampling dısında tutun. Trafik hacmi ne olursa olsun hataların %100 yakalanmasını istersiniz.
Dagıtık Izleme (Distributed Tracing) Kurulumu
OpenTelemetry Entegrasyonu
OpenTelemetry, dagıtık izleme icin satıcıdan bagımsız standarttır. .NET'te Serilog ile birlikte kullanmak her iki dunyanın da en iyisini sunar:
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(
serviceName: "OrderApi",
serviceVersion: "1.0.0"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation(options =>
{
options.Filter = httpContext =>
!httpContext.Request.Path.StartsWithSegments("/health");
})
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(options =>
{
options.SetDbStatementForText = true;
options.RecordException = true;
})
.AddSource("OrderApi.Activities")
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://otel-collector:4317");
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("OrderApi.Metrics")
.AddOtlpExporter());Ozel Activity Span'ları
Is acısından kritik operasyonlar icin zamanlama ve context takibi yapan ozel span'lar olusturun:
public class OrderProcessor
{
private static readonly ActivitySource ActivitySource = new("OrderApi.Activities");
public async Task<Order> ProcessAsync(OrderRequest request)
{
using var activity = ActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.item_count", request.Items.Count);
activity?.SetTag("order.user_id", request.UserId);
var inventory = await CheckInventory(request.Items);
activity?.AddEvent(new ActivityEvent("InventoryChecked"));
var payment = await ProcessPayment(request);
activity?.AddEvent(new ActivityEvent("PaymentProcessed"));
var order = await SaveOrder(request, payment);
activity?.SetTag("order.id", order.Id);
activity?.SetTag("order.total", order.Total);
return order;
}
}Logları Trace'lere Baglama
Dagıtık izlemenin gercek gucu, loglarınız trace span'larına baglandıgında ortaya cıkar. Serilog otomatik olarak trace ve span ID'lerini ekleyebilir:
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("ServiceName", "OrderApi")
.Enrich.With<ActivityEnricher>() // TraceId ve SpanId ekler
.CreateLogger();
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()));
}
}
}Artık bir hata logu buldugumuzda, dogrudan tracing arayuzundeki tam trace'e atlayabilir ve hataya yol acan tum servis cagrılarını gorebilirsiniz.
Alarm Stratejisi
SLO Tabanlı Alarm vs Esik Spamı
Production sistemlerde gozlemledigim kadariyla, bazı ekiplerin herkesin gormezden geldigi 200'den fazla aktif alarmı bulunuyordu. Alarm yorgunlugu gercek bir sorundur ve ham esiklere alarm baglamaktan kaynaklanır. Onemli olan SLO'larınızdır (Service Level Objectives).
Esik tabanlı alarm (sorunlu):
- CPU > %80 alarm tetikler (ama servis yuku gayet iyi karsılıyor)
- Yanıt suresi > 500ms alarm tetikler (ama tek bir sapma degeriydi)
- Herhangi bir 5xx alarm tetikler (ama hata oranı %0.001)
SLO tabanlı alarm (etkili):
- Hata butcesi tuketim hızı son bir saatte 2x'i asıyor
- 99. yuzdedilim gecikme suresi SLO'yu 10 dakika ustuste ihmal ediyor
- Kayan 1 saatlik pencerede kullanılabilirlik %99.9'un altına dusur
SLO Alarmlarının Uygulanması
// SLO takibi icin ozel metrik
public class SloMetrics
{
private static readonly Meter Meter = new("OrderApi.Metrics");
private static readonly Counter<long> RequestCounter =
Meter.CreateCounter<long>("http_requests_total");
private static readonly Counter<long> ErrorCounter =
Meter.CreateCounter<long>("http_errors_total");
private static readonly Histogram<double> LatencyHistogram =
Meter.CreateHistogram<double>("http_request_duration_seconds");
public void RecordRequest(string endpoint, int statusCode, double durationSeconds)
{
var tags = new TagList
{
{ "endpoint", endpoint },
{ "status_code", statusCode.ToString() }
};
RequestCounter.Add(1, tags);
LatencyHistogram.Record(durationSeconds, tags);
if (statusCode >= 500)
{
ErrorCounter.Add(1, tags);
}
}
}Alarm sisteminizde (Azure Monitor, Grafana vb.) burn-rate alarmları tanımlayın:
- Sayfa (birini uyandır): Hata butcesi tuketim hızı 1 saat icinde > 14.4x (aylık butcenin %2'sini 1 saatte tuketir)
- Bilet (sonraki is gunu): Hata butcesi tuketim hızı 6 saat icinde > 3x
- Bilgilendirme: Hata butcesi tuketim hızı 3 gun icinde > 1x
Bu yaklasımla sadece durum gercekten SLO'nuzu tehdit ettiginde uyarılırsınız, her gecici dalgalanmada degil.
Sık Yapılan Loglama Hataları
1. Hassas Veri Loglamak
En tehlikeli hata ve insanların kabul ettiginden daha sık yasanır:
// ASLA bunu yapmayın
_logger.LogInformation("Kullanıcı girisi: {Email}, sifre: {Password}", email, password);
_logger.LogInformation("Kart {CardNumber} ile odeme islendi", cardNumber);
_logger.LogDebug("Token {AuthToken} ile API cagrısı", authToken);
// GUVENLI alternatifler
_logger.LogInformation("Kullanıcı girisi: {Email}", email); // sifre tamamen cıkarıldı
_logger.LogInformation("Son 4 hanesi {CardLast4} olan kartla odeme islendi", cardNumber[^4..]);
_logger.LogDebug("Kullanıcı {UserId} icin API cagrısı dogrulandı", userId);Kazara hassas veri maruziyetini yakalamak icin destructure politikası ekleyin:
Log.Logger = new LoggerConfiguration()
.Destructure.ByTransforming<UserCredentials>(
creds => new { creds.Email, Password = "***GIZLI***" })
.CreateLogger();2. Eksik Correlation Context
Correlation ID olmadan, dagıtık bir sistemde "Odeme basarısız" gibi bir log satırı neredeyse islevselligini yitirir. Bunu tetikleyen istege, yasayan kullanıcıya veya sebep olan ust servise baglayamazsınız.
Correlation context'i her zaman HTTP header'ları ve mesaj kuyrugu ozellikleri uzerinden yayın.
3. Yogun Donguler Icinde Loglama
// KOTU - 10.000 ogenin her biri icin loglama
foreach (var item in items)
{
_logger.LogInformation("Oge {ItemId} isleniyor", item.Id);
await Process(item);
}
// DAHA IYI - toplu ozet loglayın
_logger.LogInformation("{ItemCount} ogeden olusan toplu islem baslatılıyor", items.Count);
var results = await ProcessBatch(items);
_logger.LogInformation("Toplu islem tamamlandı: {SuccessCount} basarılı, {FailCount} basarısız",
results.Successes, results.Failures);4. Exception'ları Yutmak
// KOTU - exception bilgisi tamamen kayboluyor
try { await ProcessOrder(order); }
catch (Exception ex)
{
_logger.LogError("Bir seyler ters gitti");
return StatusCode(500);
}
// IYI - tam exception'ı context ile koruyun
try { await ProcessOrder(order); }
catch (Exception ex)
{
_logger.LogError(ex, "Siparis {OrderId} kullanıcı {UserId} icin islenemedi",
order.Id, order.UserId);
return StatusCode(500);
}5. Log Saklama Politikası Eksikligi
Sonsuza kadar saklanan loglar hem uyumluluk riski hem de maliyet sorunudur. Saklama katmanları tanımlayın:
- Sıcak depolama (son 7 gun): Tam metin arama, hızlı sorgular
- Ilık depolama (7-30 gun): Daha yavas sorgular, dusuk maliyet
- Soguk depolama (30-90 gun): Sadece arsiv, uyumluluk saklaması
- Silme saklama suresi doldugunda
Guvenlik ve Uyumluluk
- Asla sifre, token, gizli anahtar veya zorunlu olmayan kisisel veriyi loglamayın
- Saklama politikalarını uyumluluk gereksinimlerinize (KVKK, GDPR, SOC 2) gore belirleyin
- Gelisitiricilerin sadece kendi servislerinin loglarını sorgulayabilecegi erisim kontrolleri uygulayın
- Production loglara kimlerin eristigini denetleyin
- Logları aktarımda ve depoda sifreleme ile koruyun
Sonuc
Kaliteli observability, MTTR'yi (Ortalama Onarım Suresi) onemli olcude dusurur ve olay mudahalesini tahmincilikten tekrarlanabilir bir muhendislik surecine donusturur. Structured logging, dagıtık izleme ve SLO tabanlı alarm yatırımı, bir production sorununu saatler yerine dakikalar icinde teshis ettiginizde kendini fazlasıyla geri oder.
Structured logging ve correlation ID'ler ile baslayın. Birden fazla servisiniz oldugunda dagıtık izlemeyi ekleyin. SLO tabanlı alarmları ilk gunden kurun. Bu sıralama her adımda en yuksek degeri verir.
Monitoring mimarinizi birlikte tasarlayabilirim.
İlgili Makaleler
ASP.NET Core ile RESTful API Geliştirme
ASP.NET Core ile production-ready REST API geliştirmenin temellerini öğrenin. Controller, routing ve best practice'ler.
.NET ile Mikroservis Mimarisi: Tasarım ve Uygulama
.NET ile mikroservis mimarisi tasarlayın. Service communication, Docker ve orchestration stratejileri.
.NET Performance Optimizasyonu: Profiling ve Best Practices
.NET uygulamalarının performansını optimize edin. Profiling araçları, memory management ve async patterns.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç