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ı?
| Özellik | AddHttpLogging | UseSerilogRequestLogging | AddW3CLogging |
|---|---|---|---|
| Event sayısı | Varsayılan birden fazla, CombineLogs ile bir | İstek başına bir | İstek başına bir satır |
| Header ve body | Allow list ile header, isteğe bağlı body | Yok; method, path, status ve süre | Seçilen W3C alanları, body yok |
| Zenginleştirme | Interceptor ile AddParameter | EnrichDiagnosticContext | Sınırlı ek header |
| Hedef | ILogger pipeline'ı | Serilog sink'leri | Kendi dosyası |
| Ne zaman | Hata ayıklama, belirli endpoint'lerde body | Production'da istek özeti | Eriş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.