Mert Özen Her Satırda Daha İleri
Backend .NET'te Logging — #08

ASP.NET Core'da HTTP Logging: AddHttpLogging, W3C Logger ve Request/Response Body'nin Maliyeti

Mert Özen 8 Kas 2026 9 dk 2 görüntülenme
ASP.NET Core'da HTTP Logging: AddHttpLogging, W3C Logger ve Request/Response Body'nin Maliyeti

HTTP logging'i production'da istek başına tek event, kapalı body ve yalnız hata anında body loglayan bir interceptor ile kullan. AddHttpLogging'in gerçek varsayılanları, header allow list'i, CombineLogs, [HttpLogging] ve WithHttpLogging, 4xx/5xx'te body loglayan IHttpLoggingInterceptor, W3C logger dosyası ve body loglamanın ölçülmüş maliyeti: istek başına 8 KB yerine 85 KB allocation.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; kullanılan middleware'ler ASP.NET Core 10'un kendi parçası, ek paket gerekmiyor.

Önce Tahmin Et

AddHttpLogging(o => { }) ve UseHttpLogging() ile HTTP (HyperText Transfer Protocol) logging'i varsayılan ayarlarla açtın. Bir istemci Authorization: Bearer ... header'ı ve JSON (JavaScript Object Notation) body'si olan bir POST gönderiyor. Log'da token'ı görür müsün? Body'yi? Bir de şunu düşün: body logging'i açıp bir interceptor ile başarılı isteklerde body'yi log'dan çıkarırsan, bellek maliyeti body logging'in kapalı olduğu duruma geri döner mi?

AddHttpLogging ve Varsayılanlar

HTTP logging, gelen isteği ve dönen yanıtı loglayan bir middleware. Bookstore'un katalog servisindeki yapılandırma:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Logging.AddFilter("Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware", LogLevel.Information);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = mode == "headers"
        ? HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.Duration
        : HttpLoggingFields.All;
    logging.RequestHeaders.Add("X-Correlation-ID");
    logging.MediaTypeOptions.AddText("application/problem+json");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});
if (mode == "interceptor")
{
    builder.Services.AddHttpLoggingInterceptor<BodyOnErrorInterceptor>();
}

mode değişkeni komut satırından geliyor; birazdan farklı modların maliyetini ölçeceğiz. Middleware'in category'si Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware ve log'larını Information seviyesinde yazıyor. Şablon projelerdeki "Microsoft.AspNetCore": "Warning" filtresi bu yüzden HTTP logging'i sessizce kapatır; ilk karşılaşılan "açtım ama hiçbir şey yazmıyor" sorununun sebebi budur. İlk satırdaki filtre bunu çözüyor.

Varsayılan değerleri hafızadan yazmak yerine uygulamanın içinde new HttpLoggingOptions() ve new W3CLoggerOptions() oluşturup okudum:

{"loggingFields":253,"requestBodyLogLimit":32768,"responseBodyLogLimit":32768,"combineLogs":false,"w3cFields":"Date, Time, ServerName, ConnectionInfoFields, ProtocolStatus, TimeTaken, Request","fileSizeLimit":10485760,"retainedFileCountLimit":4,"fileName":"w3clog-","flushInterval":"00:00:01"}

253, RequestPropertiesAndHeaders | ResponsePropertiesAndHeaders değerinin sayısal karşılığı: body yok, süre yok. Body limitleri 32 KB, CombineLogs kapalı. Yani baştaki sorunun ilk yarısının cevabı: varsayılan ayarla body log'a girmez.

HttpLoggingFields ve CombineLogs

CombineLogs = true olmadan middleware bir istek için ayrı ayrı event'ler yazar: request, response, request body, response body, duration. Bunlar log aracında birbirinden bağımsız satırlar olarak görünür ve aynı isteğe ait olduklarını ancak TraceId ile anlarsın. CombineLogs hepsini isteğin sonunda tek bir event'te topluyor. GET /books/1 isteğinin gerçek çıktısı:

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[9]
      Request and Response:
      Protocol: HTTP/1.1
      Method: GET
      Scheme: http
      PathBase:
      Path: /books/1
      Accept: */*
      Host: localhost:17440
      User-Agent: curl/8.7.1
      Authorization: [Redacted]
      X-Correlation-ID: web-1a2b3c4d
      StatusCode: 200
      Content-Type: application/json; charset=utf-8
      Duration: 2,4692

İstemci Authorization: Bearer secret-token gönderdi; log'da [Redacted] var. Middleware yalnız RequestHeaders ve ResponseHeaders listesindeki header'ların değerini yazıyor; listede olmayanların adını yazıp değerini gizliyor. X-Correlation-ID'yi listeye eklediğim için değeri görünüyor. Bu allow list yaklaşımı güvenli bir varsayılan: yeni bir header eklendiğinde kendiliğinden log'a girmez.

Bir ayrıntı daha: Duration: 2,4692. Süre milisaniye cinsinden ve ondalık ayırıcı virgül, çünkü makinenin culture'ı Türkçe. Bu satırı bir log aracında sayı olarak parse etmeye çalışırsan culture farkı başını ağrıtır; middleware'in event'i structured property'lerle de taşıdığını, metnin yalnız formatlanmış hâl olduğunu unutma.

Endpoint Bazlı Kural: [HttpLogging] ve WithHttpLogging

Global ayar her endpoint için doğru olmayabilir. Endpoint seviyesinde iki yol var:

app.MapGet("/ping", [HttpLogging(HttpLoggingFields.Duration)] () => "pong");

app.MapGet("/allocated", () => GC.GetTotalAllocatedBytes(precise: true)).WithHttpLogging(HttpLoggingFields.None);

[HttpLogging] attribute'u hem controller action'larında hem de minimal API (Application Programming Interface) lambda'larında çalışıyor; WithHttpLogging ise minimal API için endpoint metadata'sı ekliyor. /ping için log'a düşen tek şey:

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[9]
      Request and Response:
      Duration: 0,0765

Öncelik sırası şöyle: önce AddHttpLogging'deki global ayar, sonra endpoint metadata'sı onu ezer, en son interceptor isteğe göre bir kez daha değiştirebilir.

IHttpLoggingInterceptor: Body'yi Yalnız Hatada Loglamak

Body loglamanın gerçek değeri hata anında. "Bu sipariş neden 400 aldı?" sorusunun cevabı genellikle body'de; başarılı bir siparişin body'si ise kimsenin okumadığı megabaytlar. Interceptor ile bu kuralı yazıyoruz:

using Microsoft.AspNetCore.HttpLogging;

namespace Bookstore.Catalog;

public sealed class BodyOnErrorInterceptor : IHttpLoggingInterceptor
{
    public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext.Request.Path.StartsWithSegments("/health"))
        {
            // health check'ler her 10 saniyede bir gelir, hiç loglamıyoruz
            logContext.LoggingFields = HttpLoggingFields.None;
        }
        return default;
    }

    public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext.Response.StatusCode < 400)
        {
            // başarılı isteklerde body'yi atıyoruz, yalnız 4xx/5xx'te kalıyor
            logContext.TryDisable(HttpLoggingFields.RequestBody | HttpLoggingFields.ResponseBody);
        }
        else
        {
            logContext.AddParameter("ErrorStatus", logContext.HttpContext.Response.StatusCode);
        }
        return default;
    }
}

OnResponseAsync, yanıt başlamadan hemen önce çağrılıyor; status code bu noktada belli. TryDisable alan açıksa kapatıp true dönüyor. AddParameter ise log'a kendi alanını ekliyor. Miktarı 50 olan geçersiz bir sipariş:

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[9]
      Request and Response:
      Protocol: HTTP/1.1
      Method: POST
      Scheme: http
      PathBase:
      Path: /orders
      Accept: */*
      Host: localhost:17440
      User-Agent: curl/8.7.1
      Content-Type: application/json
      Content-Length: 74
      ErrorStatus: 400
      StatusCode: 400
      Content-Type: application/problem+json
      RequestBody: {"customerId":42,"isbn":"978-0135957059","quantity":50,"note":"gift wrap"}
      RequestBodyStatus: [Completed]
      ResponseBody: {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Quantity":["Quantity must be between 1 and 10."]},"traceId":"00-24ef6f4b429ff0c6f314b1f21c09be59-b16788b3297a2b43-00"}
      Duration: 7,5746

Hem gelen body hem de ProblemDetails yanıtı log'da; neyin yanlış gittiği tek bakışta belli. application/problem+json'ı MediaTypeOptions.AddText ile eklemem gerekti, yoksa middleware bu media type'ı metin olarak tanımıyor. Aynı siparişin geçerli hâli (miktar 2) ise body'siz:

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[9]
      Request and Response:
      Protocol: HTTP/1.1
      Method: POST
      Scheme: http
      PathBase:
      Path: /orders
      Accept: */*
      Host: localhost:17440
      User-Agent: curl/8.7.1
      Content-Type: application/json
      Content-Length: 73
      StatusCode: 201
      Content-Type: application/json; charset=utf-8
      Location: /orders/01a1b8f2-b5ad-7c28-ae98-cba6cb3526e7
      Duration: 0,4351

GET /health ise log'da hiç görünmüyor. Interceptor birden fazla olabilir; kayıt sırasıyla çalışırlar.

Body Loglamanın Maliyeti

Dokümantasyon HTTP logging'in performansı düşürebileceğini, özellikle body loglarken, açıkça yazıyor. Ne kadar? Ölçtüm. Küçük bir istemci 300 isteklik ısınmadan sonra, yaklaşık 2 KB body'li 3000 POST'u sırayla gönderiyor; sunucu ölçümden önce ve sonra GC.GetTotalAllocatedBytes değerini dönüyor:

using HttpClient http = new() { BaseAddress = new Uri("http://localhost:17440") };
string note = new('x', 2000);
object body = new { customerId = 42, isbn = "978-0135957059", quantity = 2, note };

async Task SendAsync(int count)
{
    for (int i = 0; i < count; i++)
    {
        using HttpResponseMessage response = await http.PostAsJsonAsync("/orders", body);
        response.EnsureSuccessStatusCode();
    }
}

await SendAsync(300);
long before = await http.GetFromJsonAsync<long>("/allocated");
Stopwatch stopwatch = Stopwatch.StartNew();
const int requests = 3000;
await SendAsync(requests);
stopwatch.Stop();
long after = await http.GetFromJsonAsync<long>("/allocated");

Console.WriteLine(string.Create(CultureInfo.InvariantCulture,
    $"{args[0],-12} {(after - before) / requests,8:N0} bytes/request {stopwatch.Elapsed.TotalMilliseconds / requests,7:N3} ms/request"));

Sunucuyu dört modda çalıştırdım: HTTP logging kapalı (none), yalnız header ve süre (headers), her şey dahil body (body) ve body açık ama yukarıdaki interceptor ile başarılı isteklerde atılıyor (interceptor). Console çıktısı dosyaya yönlendirildi. Sonuçlar:

none            8,296 bytes/request   0.700 ms/request
headers        12,259 bytes/request   0.711 ms/request
body           85,502 bytes/request   0.720 ms/request
interceptor    21,521 bytes/request   0.727 ms/request

Aynı 3300 istek için log dosyalarının boyutu: none 650 byte, headers 1.848.244 byte, body 15.407.894 byte, interceptor 1.528.136 byte.

Üç sonuç var. Birincisi, body logging istek başına allocation'ı yaklaşık on katına çıkardı (8 KB'tan 85 KB'a) ve log hacmini sekiz katına. Body'nin kendisi 2 KB'ken log'a düşen metin, buffer'lar ve string dönüşümleriyle çok daha büyük bir bellek izi bırakıyor. İkincisi, sıralı tek istemcide gecikme farkı ölçülemeyecek kadar küçük kaldı; console logger log'ları arka planda yazdığı için istek yolu beklemiyor. Bu, maliyetin yok olduğu anlamına gelmiyor: yük altında fazladan allocation daha sık garbage collection demek ve o maliyet bu tek istemcili ölçümde görünmüyor. Üçüncüsü ve baştaki ikinci sorunun cevabı: interceptor log hacmini header seviyesine indirdi, ama allocation 12 KB'a değil 21 KB'a indi. Middleware body'yi istek okunurken buffer'lamak zorunda; status code ise ancak yanıt başlarken belli oluyor. Atılacak body'nin de bir kez kopyalanması gerekiyor.

Bu sayılar bu makinede ve bu body boyutunda geçerli. Kendi servisin için aynı ölçümü kendi tipik body'nle yap; özellikle dosya yükleme gibi büyük body'li endpoint'lerde body logging'i WithHttpLogging ile kapatmak neredeyse her zaman doğru karar.

W3C Logger Ne Zaman?

W3C (World Wide Web Consortium) logger, IIS (Internet Information Services) ve diğer web sunucularının kullandığı W3C Extended Log File Format'ında dosyaya yazıyor. ILogger pipeline'ına girmiyor; doğrudan kendi dosyasına:

builder.Services.AddW3CLogging(w3c =>
{
    w3c.LoggingFields = W3CLoggingFields.Date | W3CLoggingFields.Time | W3CLoggingFields.Method |
        W3CLoggingFields.UriStem | W3CLoggingFields.ProtocolStatus | W3CLoggingFields.TimeTaken;
    w3c.LogDirectory = Path.Combine(builder.Environment.ContentRootPath, "w3c");
    w3c.FlushInterval = TimeSpan.FromSeconds(1);
});

app.UseW3CLogging() ile middleware'i ekledikten sonra w3c/w3clog-20261108.0000.txt dosyasının içeriği:

#Version: 1.0
#Start-Date: 2026-11-08 02:17:56
#Fields: date time cs-method cs-uri-stem sc-status time-taken
2026-11-08 02:17:56 GET /defaults 200 2.9069
2026-11-08 02:17:56 GET /books/1 200 2.1645
2026-11-08 02:17:56 GET /books/99 404 0.1062
2026-11-08 02:17:56 POST /orders 400 7.101
2026-11-08 02:17:56 POST /orders 201 0.4159
2026-11-08 02:17:56 GET /health 200 0.1659
2026-11-08 02:17:56 GET /ping 200 0.0693

Satır başına bir istek, sabit kolonlar, culture'dan bağımsız sayılar. /health burada da var, çünkü interceptor yalnız HTTP logging middleware'ini etkiliyor. Varsayılan ayarlar dosyayı 10 MB'ta döndürüyor ve son 4 dosyayı tutuyor. Bu formatı okuyan eski bir analiz aracın varsa ya da container dışında, disk üstünde çalışan bir sunucuda erişim log'u istiyorsan W3C logger mantıklı. Container dünyasında ise stdout'a giden structured log çoğu zaman daha iyi bir tercih.

UseSerilogRequestLogging ile Karşılaştırma

Serinin "Serilog Derinlemesine" yazısındaki UseSerilogRequestLogging da istek başına log yazıyor. Hangisini ne zaman kullanmalı?

ÖzellikAddHttpLoggingUseSerilogRequestLoggingAddW3CLogging
Event sayısıVarsayılan birden fazla, CombineLogs ile birİstek başına birİstek başına bir satır
Header ve bodyAllow list ile header, isteğe bağlı bodyYok; method, path, status ve süreSeçilen W3C alanları, body yok
ZenginleştirmeInterceptor ile AddParameterEnrichDiagnosticContextSınırlı ek header
HedefILogger pipeline'ıSerilog sink'leriKendi dosyası
Ne zamanHata ayıklama, belirli endpoint'lerde bodyProduction'da istek özetiErişim log'u formatı gereken yerde

Benim varsayılanım: production'da istek başına tek özet event (Serilog kullanıyorsan UseSerilogRequestLogging, kullanmıyorsan CombineLogs açık ve body kapalı HTTP logging), body'yi yalnız interceptor ile hata anında ve gerekiyorsa yalnız belirli endpoint'lerde.

Üretimde Dikkat

Middleware'in sırası önemli. UseHttpLogging'i pipeline'ın başına yakın koy; ondan önce gelen middleware'lerin işi (ör. static dosyalar) loglanmaz ve süreye dahil olmaz. Exception handler'ın önünde mi arkasında mı olduğu da 500 yanıtlarının body'sinin log'a girip girmeyeceğini belirler; kendi pipeline'ında bir kez dene.

Body'de kişisel veri ya da parola olabilir. Header'lar allow list ile korunuyor, ama body'ye böyle bir koruma yok: RequestBody açıksa login endpoint'ine gelen parola log'a girer. Login, kayıt ve ödeme gibi endpoint'lerde body logging'i WithHttpLogging ya da interceptor ile mutlaka kapat. Header ve route parametrelerini sınıflandırarak maskelemek için Microsoft.AspNetCore.Diagnostics.Middleware paketindeki AddHttpLoggingRedaction var; sınıflandırma mantığı serinin "Log'larda Hassas Veri" yazısındakiyle aynı.

Limitleri bilinçli seç. Varsayılan 32 KB'lık body limiti, istek başına 64 KB'a kadar log metni demek. Ben 4 KB'a indirdim; hata ayıklamak için çoğu zaman ilk birkaç KB yeterli.

.NET 11 ile HTTP logging middleware'inin response buffering stream'lerini pool'ladığı ve response body logging ya da interceptor açıkken istek başına allocation'ı azalttığı duyuruldu; yukarıdaki ölçüm .NET 10'da.

Sık Düşülen Tuzaklar

Birincisi, HTTP logging'i açıp "Microsoft.AspNetCore": "Warning" filtresini unutmak. Hiçbir şey yazılmaz ve kimse fark etmez; middleware'in category'sini açıkça Information'a çek.

İkincisi, HttpLoggingFields.All'ı production'da bırakmak. Yukarıdaki ölçümde log hacmi sekiz katına çıktı; log aracın ingestion'a göre ücretlendiriyorsa fatura da onunla birlikte.

Üçüncüsü, CombineLogs'u kapalı bırakıp log aracında request ve response satırlarını eşleştirmeye çalışmak. Tek event, tek sorgu.

Dördüncüsü, interceptor'ın body maliyetini tamamen sıfırladığını sanmak. Log hacmini düşürüyor ama buffering maliyeti kalıyor; body'nin hiç gerekmediği endpoint'lerde alanı endpoint seviyesinde kapat.

Küçük Bir Deneme

★☆☆ CombineLogs'u kapat ve GET /books/1 isteğini tekrar gönder. Kaç ayrı event çıkıyor ve her birinin EventId'si ne?

★★☆ Interceptor'a bir kural ekle: /auth ile başlayan path'lerde, status code ne olursa olsun RequestBody'yi hiç loglama. Kuralı OnRequestAsync'te mi OnResponseAsync'te mi yazmak daha ucuz, ölçerek karşılaştır.

★★★ Ölçümü 20 KB'lık body ile tekrarla ve RequestBodyLogLimit'i 4 KB ve 32 KB yaparak iki kez çalıştır. Allocation farkının body boyutuyla mı, limitle mi ölçeklendiğini bul.

HTTP logging'in hacmini ve maliyetini gördük. Bir sonraki yazıda log maliyetinin tamamına bakıyoruz: seviye bazlı hacim ölçümü, Microsoft.Extensions.Telemetry ile sampling ve buffering, hata olduğunda öncesindeki Debug log'larını flush etmek ve production'da log seviyesini restart'sız değiştirmek.