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

Log'larda Hassas Veri: Redaction, Data Classification ve KVKK'ya Uygun Logging

Mert Özen 8 Kas 2026 11 dk 3 görüntülenme
Log'larda Hassas Veri: Redaction, Data Classification ve KVKK'ya Uygun Logging

Hassas veriyi log'dan uzak tutmanın yolu onu sınıflandırıp logging pipeline'ında otomatik maskelemektir. Microsoft.Extensions.Compliance.Redaction ile taxonomy ve attribute'lar, [LogProperties], ErasingRedactor, kendi redactor'ın ve korelasyonu koruyan HMAC redactor'ı; Serilog'da destructuring ile maskeleme, exception message'ındaki kişisel veri ve bunu yakalayan bir FakeLogger testi gerçek çıktılarla.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Microsoft.Extensions.Telemetry 10.11, Microsoft.Extensions.Compliance.Redaction 10.11, Microsoft.Extensions.Diagnostics.Testing 10.11, Serilog 4.4 ve xUnit 2.9 ile derlenip denenmiştir.

Önce Tahmin Et

Bir log parametresini [PersonalData] diye işaretledin, ama logging tarafında redaction'ı açmayı unuttun; EnableRedaction() çağrısı yok. Müşterinin e-posta adresi log'a açık metin olarak mı düşer, boş mu gelir, yoksa uygulama hata mı verir? Bir de şunu düşün: redaction açıkken aynı e-posta adresi iki farklı event'te loglanıyor. Maskelenmiş iki değer birbirinin aynısı mı olur?

Log'a Asla Girmemesi Gerekenler

Bazı veriler log'a hiçbir koşulda girmemeli: parola, access token ve refresh token, API (Application Programming Interface) key, kart numarası ve CVV (Card Verification Value), TC (Türkiye Cumhuriyeti) kimlik numarası. Bunların log'da görünmesi, log sistemine erişimi olan herkesin, log'ları yedekleyen her sistemin ve log'ları satın aldığın SaaS (Software as a Service) aracının bu veriye erişmesi demek. Log'lar genellikle veritabanından çok daha geniş bir kitleye açıktır ve çok daha az korunur.

İkinci grup kişisel veri: ad soyad, e-posta, telefon, adres, IP (Internet Protocol) adresi. Bunlar bazen gerçekten gerekir; "bu müşterinin şikâyetini incele" diyen destek ekibi müşteriyi bulabilmeli. Ama log'a kişisel veri yazdığın anda o log da bir kişisel veri işleme faaliyeti olur. KVKK (Kişisel Verilerin Korunması Kanunu) ve Avrupa'daki karşılığı GDPR (General Data Protection Regulation), genel çerçevede, kişisel verinin amaçla sınırlı, ölçülü ve yalnız gerektiği süre kadar saklanmasını ister. Log'ların retention süresi, kimin erişebildiği ve silme talebinde ne yapılacağı bu çerçevenin parçası. Bu bir hukuki değerlendirme değil; kendi durumun için hukuk ekibine danış. Teknik tarafta yapabileceğin en iyi şey kişisel veriyi log'a hiç koymamak, koymak zorundaysan da maskelemek.

Güvenlik açısından neyin loglanması gerektiğini OWASP (Open Worldwide Application Security Project) Top 10 üzerine kurulu Web Güvenliği serisinin "OWASP A09: Security Logging and Alerting Failures — Neyi Loglamalı, Neyi Asla" yazısında konuştuk. Bu yazı mekanizmaya odaklanıyor: Microsoft.Extensions.Compliance.Redaction ile veriyi sınıflandırmak ve logging pipeline'ında otomatik maskelemek.

Data Classification: Taxonomy ve Attribute'lar

Redaction'ın temeli veriyi sınıflandırmak. Önce kendi taxonomy'ni tanımlıyorsun, sonra her sınıf için bir attribute:

using Microsoft.Extensions.Compliance.Classification;

namespace Bookstore.Customers;

public static class BookstoreTaxonomy
{
    public static string Name => "Bookstore";

    public static DataClassification PersonalData => new(Name, nameof(PersonalData));
    public static DataClassification SensitiveData => new(Name, nameof(SensitiveData));
}

public sealed class PersonalDataAttribute() : DataClassificationAttribute(BookstoreTaxonomy.PersonalData);

public sealed class SensitiveDataAttribute() : DataClassificationAttribute(BookstoreTaxonomy.SensitiveData);

Microsoft Learn'deki redaction örneği parametreyi doğrudan [MyTaxonomyClassifications.Private] diye işaretliyor. Bu derlenmez: C#'ta attribute olarak yalnız Attribute'tan türeyen bir class kullanılabilir, bir static property değil. DataClassificationAttribute'tan türeyen küçük class'lar bu boşluğu dolduruyor; primary constructor ile her biri tek satır.

Ben iki sınıf yeterli buldum. PersonalData kişiyi tanımlayan ama bazen korelasyon için gereken veri (e-posta, ad soyad); SensitiveData ise hiçbir koşulda görünmemesi gereken veri (kimlik numarası). Sınıf sayısını küçük tut; her sınıf, ekipte herkesin ezberlemesi gereken bir kural demek.

LoggerMessage ve [LogProperties] ile Sınıflandırılmış Parametreler

Sınıflandırmayı iki yerde kullanıyoruz: bir log method'unun parametresinde ve log'a açılan bir object'in property'lerinde:

namespace Bookstore.Customers;

public sealed class CustomerSnapshot
{
    public int CustomerId { get; init; }

    [PersonalData]
    public string Email { get; init; } = "";

    [PersonalData]
    public string FullName { get; init; } = "";

    [SensitiveData]
    public string NationalId { get; init; } = "";

    public string City { get; init; } = "";
}
using Microsoft.Extensions.Logging;

namespace Bookstore.Customers;

public static partial class Log
{
    [LoggerMessage(2001, LogLevel.Information, "Customer {CustomerId} registered with email {Email}")]
    public static partial void CustomerRegistered(ILogger logger, int customerId, [PersonalData] string email);

    [LoggerMessage(2002, LogLevel.Information, "Customer signed in")]
    public static partial void CustomerSignedIn(ILogger logger, [LogProperties] CustomerSnapshot customer);

    [LoggerMessage(2003, LogLevel.Error, "Registration failed for customer {CustomerId}")]
    public static partial void RegistrationFailed(ILogger logger, Exception exception, int customerId);
}

[LogProperties] object'in public property'lerini customer.Email, customer.City gibi ayrı log property'lerine açıyor. Bunu serinin "High-Performance Logging" yazısında görmüştük; orada "hassas alanlara dikkat" diye bıraktığımız konu tam olarak bu. [LogProperties] ve sınıflandırılmış parametreler, standart source generator'ın değil Microsoft.Extensions.Telemetry.Abstractions paketindeki genişletilmiş generator'ın özelliği; paket projeye girdiğinde generator kendiliğinden devreye giriyor.

Şimdi baştaki ilk sorunun cevabı. EnableRedaction() olmadan, sadece AddJsonConsole() ile çalıştırdığımda çıkan ilk iki satır:

{"EventId":2001,"LogLevel":"Information","Category":"Bookstore.Customers","Message":"Customer 42 registered with email (null)","State":{"Message":"CustomerId=42,{OriginalFormat}=Customer {CustomerId} registered with email {Email},Email=\u003Comitted\u003E ([Bookstore:PersonalData])","CustomerId":42,"{OriginalFormat}":"Customer {CustomerId} registered with email {Email}"}}
{"EventId":2002,"LogLevel":"Information","Category":"Bookstore.Customers","Message":"Customer signed in","State":{"Message":"customer.City=Izmir,customer.CustomerId=42,{OriginalFormat}=Customer signed in,customer.NationalId=\u003Comitted\u003E ([Bookstore:SensitiveData]),customer.FullName=\u003Comitted\u003E ([Bookstore:PersonalData]),customer.Email=\u003Comitted\u003E ([Bookstore:PersonalData])","customer.City":"Izmir","customer.CustomerId":42,"{OriginalFormat}":"Customer signed in"}}

E-posta sızmadı. Sınıflandırılmış değerler hiç property olarak çıkmadı; mesajda (null), metin hâlinde ise <omitted> ([Bookstore:PersonalData]) var. Davranış güvenli tarafta kalıyor: redaction'ı açmayı unutursan veri kaybolur ama açığa çıkmaz. Sınıflandırılmamış City ve CustomerId ise normal geliyor.

EnableRedaction ve Redactor'lar

Redaction iki parçadan oluşuyor: logging pipeline'ında açmak ve hangi sınıfa hangi redactor'ın uygulanacağını söylemek:

bool redact = args.Contains("--redact");

ServiceCollection services = new();
services.AddLogging(logging =>
{
    logging.AddJsonConsole();
    if (redact)
    {
        logging.EnableRedaction();
    }
});
services.AddRedaction(redaction =>
{
    redaction.SetRedactor<ErasingRedactor>(BookstoreTaxonomy.SensitiveData);
    redaction.SetHmacRedactor(options =>
    {
        options.KeyId = 101;
        options.Key = Environment.GetEnvironmentVariable("BOOKSTORE_HMAC_KEY")!;
    }, BookstoreTaxonomy.PersonalData);
});

ErasingRedactor değeri boş string'e çeviriyor. HMAC (Hash-based Message Authentication Code) redactor'ı ise değeri gizli bir key ile hash'liyor. Bir sınıf için redactor tanımlamazsan varsayılan fallback yine ErasingRedactor. --redact ile çalıştırınca:

{"EventId":2001,"LogLevel":"Information","Category":"Bookstore.Customers","Message":"Customer 42 registered with email 101:nxi4FBX2XMdo97T/Cc1dKA==","State":{"Message":"Microsoft.Extensions.Logging.ExtendedLogger\u002BModernTagJoiner","Email":"101:nxi4FBX2XMdo97T/Cc1dKA==","CustomerId":42,"{OriginalFormat}":"Customer {CustomerId} registered with email {Email}"}}
{"EventId":2002,"LogLevel":"Information","Category":"Bookstore.Customers","Message":"Customer signed in","State":{"Message":"Microsoft.Extensions.Logging.ExtendedLogger\u002BModernTagJoiner","customer.NationalId":"","customer.FullName":"101:OzRdd1hZqtxqhLX9KgrWXw==","customer.Email":"101:gii2QzGZ2TvnZSDfmTSBKw==","customer.City":"Izmir","customer.CustomerId":42,"{OriginalFormat}":"Customer signed in"}}
{"EventId":2001,"LogLevel":"Information","Category":"Bookstore.Customers","Message":"Customer 43 registered with email 101:nxi4FBX2XMdo97T/Cc1dKA==","State":{"Message":"Microsoft.Extensions.Logging.ExtendedLogger\u002BModernTagJoiner","Email":"101:nxi4FBX2XMdo97T/Cc1dKA==","CustomerId":43,"{OriginalFormat}":"Customer {CustomerId} registered with email {Email}"}}

NationalId boş, e-posta ve ad soyad 101: önekiyle hash'lenmiş; 101 bizim KeyId'miz. JSON (JavaScript Object Notation) console'un State.Message alanında bir class adı görmen kafanı karıştırmasın: EnableRedaction logger'ı genişletilmiş logger ile değiştiriyor ve JSON formatter bu state'in ToString()'ini yazıyor. Asıl değerler property'lerde.

Boş string bazen yanıltıcı olabilir: alan gerçekten boş muydu, yoksa silindi mi? Kendi redactor'ını yazmak birkaç satır:

using Microsoft.Extensions.Compliance.Redaction;

namespace Bookstore.Customers;

public sealed class StarRedactor : Redactor
{
    private const string Stars = "****";

    public override int GetRedactedLength(ReadOnlySpan<char> input) => Stars.Length;

    public override int Redact(ReadOnlySpan<char> source, Span<char> destination)
    {
        Stars.CopyTo(destination);
        return Stars.Length;
    }
}

SetRedactor<ErasingRedactor> yerine SetRedactor<StarRedactor> yazınca "customer.NationalId":"****" geliyor. Sabit uzunluk bilinçli bir tercih: new string('*', input.Length) gibi bir maske değerin uzunluğunu sızdırır.

HMAC Redactor: Maskele ama Korelasyonu Koru

HMAC redactor'ın asıl değeri şu: aynı girdi, aynı key ile her zaman aynı çıktıyı veriyor. Yukarıda müşteri 42 ve 43'ün Email değeri aynı (nxi4FBX2...), çünkü ikisinde de aynı adresi kullandım. Destek ekibi "bu e-posta adresiyle kaç kayıt denemesi olmuş?" diye sorduğunda, adresi aynı key ile hash'leyip log'da arayabilir; log'u okuyan biri ise adresi geri çıkaramaz.

Ama baştaki ikinci soruya dikkat: Email parametresi nxi4FBX2..., aynı adresin customer.Email property'si ise gii2QzGZ.... Değer aynı, hash farklı. Sebep LoggerRedactionOptions.ApplyDiscriminator: varsayılanı true ve property adını hash'e katıyor. Amaç, farklı adlı alanlar arasında korelasyonu bilerek zorlaştırmak. Aynı adın farklı event'lerde aynı hash'i vermesi için property adlarını tutarlı seçmek yeterli. Alanlar arası korelasyon gerçekten gerekiyorsa:

logging.EnableRedaction(o => o.ApplyDiscriminator = false);

Bu ayarla aynı key altında üç değer de eşitlendi: Email, customer.Email ve redactor'ı doğrudan çağırdığımda dönen değer 101:6n1N4E1fLR2NVXihvTI/FA== oldu (bu koşuda key farklı olduğu için değer de farklı).

Key ile ilgili kurallar Microsoft Learn'de net: base64 biçiminde ve en az 44 karakter olmalı, her büyük deployment için ayrı olmalı, gizli tutulmalı ve düzenli döndürülmeli. Key'i koda ya da appsettings'e yazma; ben environment variable'dan okudum, production'da secret store kullan. KeyId her değerin önüne yazıldığı için key döndürdüğünde hangi değerin hangi key ile üretildiği görünür; farklı KeyId'li değerler birbiriyle karşılaştırılamaz.

Bir not: dokümantasyon SetHmacRedactor'ın deneysel olduğunu ve EXTEXP0002 uyarısı ürettiğini söylüyor. 10.11.0 paketiyle derlediğimde bu uyarı çıkmadı; build "0 Warning(s)" ile bitti.

Hash'lemenin sınırını da bil: e-posta adresleri ve TC kimlik numaraları gibi tahmin edilebilir değerler, key ele geçirilirse kaba kuvvetle geri bulunabilir. HMAC'i ancak key gizli kaldığı sürece güvenli say.

Serilog'da Destructuring ile Maskelemek

Serilog kullanıyorsan Microsoft'un redaction pipeline'ı Serilog'un kendi {@Customer} destructuring'ine uygulanmaz. Orada maskeleme destructuring policy ile yapılıyor:

using Serilog;

namespace Bookstore.Customers;

public static class SerilogDemo
{
    public static void Run(CustomerSnapshot customer)
    {
        using Serilog.Core.Logger unsafeLogger = new LoggerConfiguration()
            .WriteTo.Console()
            .CreateLogger();
        unsafeLogger.Information("Customer signed in {@Customer}", customer);

        using Serilog.Core.Logger safeLogger = new LoggerConfiguration()
            .Destructure.ByTransforming<CustomerSnapshot>(c => new { c.CustomerId, c.City, Email = MaskEmail(c.Email) })
            .WriteTo.Console()
            .CreateLogger();
        safeLogger.Information("Customer signed in {@Customer}", customer);
    }

    private static string MaskEmail(string email)
    {
        int at = email.IndexOf('@');
        return at <= 1 ? "***" : $"{email[0]}***{email[at..]}";
    }
}
[05:12:02 INF] Customer signed in {"CustomerId": 42, "Email": "ada@example.com", "FullName": "Ada Lovelace", "NationalId": "10000000146", "City": "Izmir", "$type": "CustomerSnapshot"}
[05:12:02 INF] Customer signed in {"CustomerId": 42, "City": "Izmir", "Email": "a***@example.com"}

İlk satır, @ operatörünün ne kadar tehlikeli olabileceğini gösteriyor: object'te ne varsa log'da. İkinci satırda yalnız izin verdiğimiz alanlar var. ByTransforming bir allow list çalışıyor; class'a yarın yeni bir alan eklenirse varsayılan olarak log'a girmez. Bu, "hassas alanları çıkar" yaklaşımından daha güvenli. Serilog yapılandırmasının bütünü serinin "Serilog Derinlemesine" yazısında.

Exception Message'ındaki Kişisel Veri

Redaction yalnız sınıflandırdığın parametrelere ve property'lere uygulanır. En sık gözden kaçan kaçak yol exception message'ı:

try
{
    throw new InvalidOperationException($"Customer with email {ada.Email} already exists");
}
catch (InvalidOperationException ex)
{
    Log.RegistrationFailed(logger, ex, ada.CustomerId);
}
{"EventId":2003,"LogLevel":"Error","Category":"Bookstore.Customers","Message":"Registration failed for customer 42","Exception":"System.InvalidOperationException: Customer with email ada@example.com already exists\n   at Program.\u003CMain\u003E$(String[] args) in /src/Bookstore.Customers/Program.cs:line 47","State":{"Message":"Microsoft.Extensions.Logging.ExtendedLogger\u002BModernTagJoiner","exception.type":"System.InvalidOperationException","CustomerId":42,"{OriginalFormat}":"Registration failed for customer {CustomerId}"}}

Redaction açık, log method'u temiz, ama e-posta adresi Exception alanında açık metin. Aynı sorun Entity Framework Core'un (EF Core) constraint ihlali mesajlarında ya da bir dış servisin hata gövdesini exception'a koyan HTTP (HyperText Transfer Protocol) client'larda da çıkar. Çözüm exception'ı oluşturan yerde: message'a kişisel veri değil, kimlik koy (Customer 42 is already registered).

Testle Doğrulamak: FakeLogger

Kurallar ancak test edildiğinde kalıcı olur. Microsoft.Extensions.Diagnostics.Testing paketindeki FakeLogger, gerçek logging pipeline'ından (redaction dahil) geçen kayıtları bellekte topluyor:

public sealed class RedactionTests
{
    private const string Email = "ada@example.com";

    private static (ILogger Logger, FakeLogCollector Collector, ServiceProvider Provider) CreateLogger()
    {
        ServiceCollection services = new();
        services.AddLogging(logging => logging.EnableRedaction());
        services.AddFakeLogging();
        services.AddRedaction(redaction =>
            redaction.SetRedactor<StarRedactor>(BookstoreTaxonomy.PersonalData, BookstoreTaxonomy.SensitiveData));
        ServiceProvider provider = services.BuildServiceProvider();
        return (provider.GetRequiredService<ILogger<RedactionTests>>(), provider.GetFakeLogCollector(), provider);
    }

    private static void AssertNoEmail(FakeLogCollector collector)
    {
        foreach (FakeLogRecord record in collector.GetSnapshot())
        {
            Assert.DoesNotContain(Email, record.Message);
            Assert.DoesNotContain(Email, record.Exception?.ToString() ?? "");
            Assert.All(record.StructuredState ?? [], pair => Assert.DoesNotContain(Email, pair.Value ?? ""));
        }
    }

    [Fact]
    public void Customer_events_do_not_contain_the_email()
    {
        (ILogger logger, FakeLogCollector collector, ServiceProvider provider) = CreateLogger();
        using ServiceProvider _ = provider;

        Log.CustomerRegistered(logger, 42, Email);
        Log.CustomerSignedIn(logger, new CustomerSnapshot { CustomerId = 42, Email = Email, FullName = "Ada Lovelace" });

        Assert.Equal(2, collector.Count);
        Assert.Equal("Customer 42 registered with email ****", collector.GetSnapshot()[0].Message);
        AssertNoEmail(collector);
    }

    [Fact]
    public void Registration_failure_does_not_contain_the_email()
    {
        (ILogger logger, FakeLogCollector collector, ServiceProvider provider) = CreateLogger();
        using ServiceProvider _ = provider;

        try
        {
            CustomerRegistry.Register(42, Email);
        }
        catch (InvalidOperationException ex)
        {
            Log.RegistrationFailed(logger, ex, 42);
        }

        AssertNoEmail(collector);
    }
}

İkinci test, exception'ı oluşturan gerçek kodu çağırıyor:

public static class CustomerRegistry
{
    public static void Register(int customerId, string email)
    {
        // her e-posta zaten kayıtlıymış gibi davranıyoruz
        throw new InvalidOperationException($"Customer with email {email} already exists");
    }
}

dotnet test çıktısı:

[xUnit.net 00:00:00.08]     Bookstore.Customers.Tests.RedactionTests.Registration_failure_does_not_contain_the_email [FAIL]
  Failed Bookstore.Customers.Tests.RedactionTests.Registration_failure_does_not_contain_the_email [3 ms]
  Error Message:
   Assert.DoesNotContain() Failure: Sub-string found
                                ↓ (pos 54)
String: ···"Customer with email ada@example.com alrea"···
Found:  "ada@example.com"
Failed!  - Failed:     1, Passed:     1, Skipped:     0, Total:     2, Duration: 31 ms - Bookstore.Customers.Tests.dll (net10.0)

Test kaçağı yakaladı. Message'ı $"Customer {customerId} is already registered" yapınca:

Passed!  - Failed:     0, Passed:     2, Skipped:     0, Total:     2, Duration: 28 ms - Bookstore.Customers.Tests.dll (net10.0)

Bu testi bir kez yazıp bırakma; yeni bir log event'i eklendiğinde aynı yardımcı method'la kapsama al. Bir regresyon, bir code review'dan çok daha güvenilir bir bekçi.

Üretimde Dikkat

HTTP logging de bir sızıntı kaynağı: Authorization header'ı, cookie'ler ve request body'deki parola. ASP.NET Core'un HTTP logging middleware'i yalnız izin listesindeki header'ların değerini yazıyor, diğerlerine [Redacted] diyor; Microsoft.AspNetCore.Diagnostics.Middleware paketindeki AddHttpLoggingRedaction ise header'ları ve route parametrelerini bu yazıdaki taxonomy ile sınıflandırmana izin veriyor. Ayrıntısı serinin bir sonraki yazısında.

Redaction'ın işlemci maliyeti var; HMAC her değer için bir hash hesaplıyor. Her istekte onlarca sınıflandırılmış alan logluyorsan ölç. Çoğu durumda daha ucuz çözüm o alanı hiç loglamamak.

Retention'ı unutma. Maskelenmiş bile olsa kişisel veriyle ilişkilendirilebilen log'ları gereğinden uzun tutmak hem maliyet hem de uyum riski. Log aracında retention politikasını sınıflara göre ayırmak mümkünse (ör. audit log'ları daha uzun, debug log'ları kısa), kullan; Seq'te bunu serinin "Seq ile Structured Log'ları Okumak" yazısında yapıyoruz.

Key'i döndürdüğünde eski ve yeni log'lar artık eşleşmez. Destek ekibinin "son 90 gün" sorgusunu yeni key ile yapacağını varsayıyorsan, rotasyon takvimini retention süresiyle birlikte düşün.

Sık Düşülen Tuzaklar

Birincisi, EnableRedaction'ın extension method'larla yazılan log'ları da koruduğunu sanmak. logger.LogInformation("Email {Email}", email) çağrısında parametrenin bir sınıflandırması yok; redaction açık olsa bile e-posta açık metin olarak gider. Sınıflandırma ancak source-generated log method'larında ve [LogProperties] ile açılan type'larda çalışır.

İkincisi, ToString()'i override edilmiş bir object'i loglamak. Bugün Customer.ToString() yalnız id dönüyor; yarın birisi hata ayıklamak için oraya e-postayı ekler ve her log satırına sızar.

Üçüncüsü, maskeyi yalnız mesaj metninde yapıp property'yi unutmak. Elle yazılmış bir "a***@example.com" mesajın yanında, aynı event'in property'sinde tam adres durabilir. Structured log'da sızıntıyı mesajda değil property'lerde ara.

Dördüncüsü, test ortamında farklı redactor kullanıp production davranışını varsaymak. Testte StarRedactor, production'da HMAC kullanıyorum; test "değer sızmıyor" kuralını doğruluyor, hash biçimini değil. HMAC'e özgü bir beklentin varsa onu ayrı bir testle doğrula.

Küçük Bir Deneme

★☆☆ CustomerSnapshot'a Phone adında bir property ekle ve sınıflandırma koyma. --redact ile çalıştır ve telefonun log'da açık metin göründüğünü gör. Sonra [PersonalData] ekleyip tekrar çalıştır.

★★☆ RedactionTests'e üçüncü bir test ekle: logger.LogInformation("Email {Email}", Email) çağrısı yapıp AssertNoEmail'i çalıştır. Testin neden kırıldığını yukarıdaki ilk tuzakla açıkla.

★★★ HMAC key'ini iki farklı KeyId ile döndür: aynı process'te önce 101, sonra 102 ile aynı e-postayı logla. Destek ekibinin her iki dönemi de bulabilmesi için hangi bilginin saklanması gerektiğini yaz.

Hassas veriyi log method'larında ve object'lerde kontrol altına aldık. Bir sonraki yazıda bütün bir HTTP isteğini ve yanıtını loglayan middleware'e bakıyoruz: AddHttpLogging, HttpLoggingFields, IHttpLoggingInterceptor ile yalnız 4xx/5xx yanıtlarda body loglamak, W3C (World Wide Web Consortium) logger ve request/response body loglamanın ölçülmüş maliyeti.