Üretimde Serilog: PII Temizlemeli Çok Dosyalı Günlükleme
Günlükleme (logging), her geliştiricinin iyi yapması gerektiğini bildiği ama çok azının doğru yapmaya gerçekten zaman ayırdığı şeylerden biri. Bankacılık API'mizde günlükleme yalnızca hata ayıklama için değil — yasal bir gereklilik. Denetim izlerine, hata takibine, performans izlemeye ve güvenlik olayı günlüklerine ihtiyacımız var. Ancak günlük dosyalarında asla görünmemesi gereken kişisel olarak tanımlanabilir bilgileri (PII) de işliyoruz. İşte Serilog'u, beş adanmış günlük dosyası ve otomatik PII temizleme ile bunların hepsini halledecek şekilde nasıl yapılandırdığımı anlatıyorum.
Beş Dosya Stratejisi
Tek bir devasa günlük dosyası yerine, günlükleri amaçlarına göre ayırıyoruz:
csharp// Program.cs
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("Application", "BankingAPI")
// 1. General application log
.WriteTo.File(
path: "logs/application-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{RequestId}] {Message:lj}{NewLine}{Exception}")
// 2. Error-only log for alerting
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(e => e.Level >= LogEventLevel.Error)
.WriteTo.File(
path: "logs/errors-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 90,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{RequestId}] {SourceContext} {Message:lj}{NewLine}{Exception}"))
// 3. Security events
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(e =>
e.Properties.ContainsKey("SecurityEvent"))
.WriteTo.File(
path: "logs/security-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 365,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{RequestId}] [SEC] {Message:lj}{NewLine}"))
// 4. Performance metrics
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(e =>
e.Properties.ContainsKey("ElapsedMs"))
.WriteTo.File(
path: "logs/performance-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {RequestId} {Method} {Path} {StatusCode} {ElapsedMs}ms{NewLine}"))
// 5. Audit trail
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(e =>
e.Properties.ContainsKey("AuditEvent"))
.WriteTo.File(
path: "logs/audit-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 365 * 2, // 2 years for compliance
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{RequestId}] [AUDIT] UserId={UserId} Action={AuditAction} {Message:lj}{NewLine}"))
.CreateLogger();
Her dosyanın farklı bir saklama politikası vardır:
- Uygulama günlükleri: 30 gün — genel hata ayıklama, agresif biçimde döndürülür
- Hata günlükleri: 90 gün — olay (incident) incelemesi için daha uzun saklama
- Güvenlik günlükleri: 1 yıl — güvenlik olayları için yasal gereklilik
- Performans günlükleri: 30 gün — eğilim (trend) analizi için kullanılır
- Denetim günlükleri: 2 yıl — finansal işlemler için uyumluluk (compliance) gerekliliği
Günlük Bazında Döndürme (Daily Rolling) Yapılandırması
RollingInterval.Day ayarı, her gün tarihi eklenmiş yeni bir dosya oluşturur: application-20250301.log, application-20250302.log vb. retainedFileCountLimit ile birleştiğinde, eski dosyalar otomatik olarak silinir.
Ancak çoğu eğitimin atladığı bir incelik var — dosya boyutu sınırları. Yoğun bir API günde gigabaytlarca günlük üretebilir. Günlük döndürmenin üzerine dosya boyutu döndürmesi de ekliyoruz:
csharp.WriteTo.File(
path: "logs/application-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
fileSizeLimitBytes: 100_000_000, // 100 MB per file
rollOnFileSizeLimit: true, // Create application-20250301_001.log
outputTemplate: "...")
PII Temizleme: Yapı Çözme (Destructuring) Yaklaşımı
Günlükleme kurulumumuzun en kritik yönü PII temizlemesidir. Hesap numaralarının, kimlik numaralarının veya telefon numaralarının günlük dosyalarında görünmesine izin veremeyiz. Şifrelenmiş depolama olsa bile, günlüklerdeki PII bir uyumluluk ihlalidir.
Özel bir Serilog yapı çözme (destructuring) politikası kullanıyorum:
csharp// Logging/PiiSanitizingDestructuringPolicy.cs
public class PiiSanitizingDestructuringPolicy : IDestructuringPolicy
{
private static readonly HashSet<string> PiiProperties = new(
StringComparer.OrdinalIgnoreCase)
{
"Password", "Token", "RefreshToken", "AccessToken",
"NationalId", "Ssn", "TaxId",
"AccountNumber", "Iban", "CardNumber", "Cvv", "Pin",
"PhoneNumber", "MobileNumber", "Phone",
"Email", "EmailAddress",
"FirstName", "LastName", "FullName",
"Address", "Street", "City",
"DateOfBirth", "BirthDate",
};
public bool TryDestructure(
object value,
ILogEventPropertyValueFactory propertyValueFactory,
out LogEventPropertyValue? result)
{
result = null;
if (value == null) return false;
var type = value.GetType();
if (type.IsPrimitive || type == typeof(string)) return false;
var properties = type.GetProperties(
BindingFlags.Public | BindingFlags.Instance);
var sanitizedProps = new List<LogEventProperty>();
foreach (var prop in properties)
{
var propValue = prop.GetValue(value);
if (PiiProperties.Contains(prop.Name))
{
var masked = MaskValue(prop.Name, propValue?.ToString());
sanitizedProps.Add(new LogEventProperty(
prop.Name,
propertyValueFactory.CreatePropertyValue(masked)));
}
else
{
sanitizedProps.Add(new LogEventProperty(
prop.Name,
propertyValueFactory.CreatePropertyValue(
propValue, destructureObjects: true)));
}
}
result = new StructureValue(sanitizedProps);
return true;
}
private static string MaskValue(string propertyName, string? value)
{
if (string.IsNullOrEmpty(value)) return "***";
return propertyName.ToLower() switch
{
"email" or "emailaddress" =>
MaskEmail(value), // u***@example.com
"phonenumber" or "mobilenumber" or "phone" =>
MaskPhone(value), // +90*****4567
"accountnumber" or "iban" =>
MaskAccount(value), // ******7890
"cardnumber" =>
MaskCard(value), // ****-****-****-1234
_ => "***REDACTED***"
};
}
private static string MaskEmail(string email)
{
var parts = email.Split('@');
if (parts.Length != 2) return "***@***";
return $"{parts[0][0]}***@{parts[1]}";
}
private static string MaskPhone(string phone)
{
if (phone.Length < 4) return "***";
return phone[..3] + new string('*', phone.Length - 7) + phone[^4..];
}
private static string MaskAccount(string account)
{
if (account.Length < 4) return "***";
return new string('*', account.Length - 4) + account[^4..];
}
private static string MaskCard(string card)
{
var digits = new string(card.Where(char.IsDigit).ToArray());
if (digits.Length < 4) return "****";
return $"****-****-****-{digits[^4..]}";
}
}
Yapılandırmada kaydedin:
csharpLog.Logger = new LoggerConfiguration()
.Destructure.With<PiiSanitizingDestructuringPolicy>()
// ... rest of configuration
.CreateLogger();
Artık bir kullanıcı nesnesini günlüğe kaydettiğinizde:
csharp_logger.LogInformation("User authenticated: {@User}", user);
// Output:
// User authenticated: { UserId: "usr_123", Email: "u***@example.com",
// PhoneNumber: "+90*****4567", NationalId: "***REDACTED***",
// Role: "Customer", LastLogin: "2025-03-01T10:00:00Z" }
PII olmayan alanlar (UserId, Role, LastLogin) değişmeden geçer. PII alanları otomatik olarak maskelenir.
Güvenlik Olayı Günlüğü
Güvenlik olayları için, filtreleme amacıyla SecurityEvent özelliğini ekleyen adanmış bir logger uzantısı (extension) kullanırız:
csharppublic static class SecurityLogExtensions
{
public static void LogSecurityEvent(
this ILogger logger,
string eventType,
string description,
object? details = null)
{
using (LogContext.PushProperty("SecurityEvent", true))
using (LogContext.PushProperty("SecurityEventType", eventType))
{
logger.LogWarning(
"Security Event [{EventType}]: {Description} {@Details}",
eventType, description, details);
}
}
}
// Usage in middleware
_logger.LogSecurityEvent(
"BRUTE_FORCE_DETECTED",
"Multiple failed login attempts",
new { IpAddress = remoteIp, AttemptCount = 5, UserId = userId });
Bu olay, otomatik olarak hem logs/security-*.log'a (SecurityEvent filtresi aracılığıyla) hem de logs/application-*.log'a (minimum seviyeyi karşıladığı için) yönlendirilir.
Uyumluluk İçin Denetim İzi (Audit Trail)
Finansal işlemler, değiştirilemez (immutable) bir denetim izi gerektirir:
csharppublic class AuditLogger
{
private readonly ILogger<AuditLogger> _logger;
public void LogTransaction(
string userId,
string action,
decimal amount,
string fromAccount,
string toAccount)
{
using (LogContext.PushProperty("AuditEvent", true))
using (LogContext.PushProperty("UserId", userId))
using (LogContext.PushProperty("AuditAction", action))
{
_logger.LogInformation(
"Transaction: {Action} Amount={Amount} " +
"From={FromAccount} To={ToAccount}",
action,
amount,
MaskAccount(fromAccount),
MaskAccount(toAccount));
}
}
}
// Output in audit log:
// 2025-03-01 10:30:15.123 [req_abc123] [AUDIT] UserId=usr_456
// Action=TRANSFER Transaction: TRANSFER Amount=1500.00
// From=******7890 To=******3456
Denetim günlüklerinde bile hesap numaralarının maskelendiğine dikkat edin. Denetim izi, kimin neyi ne zaman yaptığını kaydeder, ancak tam hesap numaraları yalnızca veritabanında, rol tabanlı erişimle korunarak yaşar.
Performans Günlükleme Middleware'i
Performans günlüğümüz, istek zamanlamasını otomatik olarak yakalar:
csharppublic class PerformanceLoggingMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
await _next(context);
sw.Stop();
using (LogContext.PushProperty("ElapsedMs", sw.ElapsedMilliseconds))
using (LogContext.PushProperty("Method", context.Request.Method))
using (LogContext.PushProperty("Path", context.Request.Path))
using (LogContext.PushProperty("StatusCode", context.Response.StatusCode))
{
if (sw.ElapsedMilliseconds > 1000)
{
_logger.LogWarning(
"Slow request: {Method} {Path} completed in {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
sw.ElapsedMilliseconds);
}
else
{
_logger.LogInformation(
"Request: {Method} {Path} completed in {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
sw.ElapsedMilliseconds);
}
}
}
}
1 saniyenin üzerindeki istekler uyarı (warning) olarak günlüğe kaydedilir; bu da izleme araçlarında onları filtrelemeyi kolaylaştırır. Performans günlük dosyası, analiz için bize CSV benzeri temiz bir biçim sunar.
Üretimden Dersler
Bu yapılandırmayı bir yıl çalıştırdıktan sonra ortaya çıkan temel dersler şunlar. İlk olarak, günlük hacmi yönetimi kritiktir — dosya boyutu sınırları olmadan, en yoğun günümüz 12GB uygulama günlüğü üretti. 100MB dosya boyutu sınırı, tek tek dosyaları yönetilebilir tutar. İkincisi, PII sızıntıları beklenmedik yerlerden gelir. Başlangıçta, hassas alanlar içeren entity sınıflarındaki ToString() override'larını kaçırmıştık. Yapı çözme politikası, bunları serileştirme (serialization) sınırında yakalar. Üçüncüsü, yapısal günlükleme (structured logging) karşılığını verir. Serilog'un yapısal biçimini ({@User}, string interpolasyonu yerine) kullandığımız için, olayları incelerken günlükleri programatik olarak sorgulayabiliriz. Dördüncüsü, ayrı amaçlar için ayrı dosyalar, bir performans sorununu inceleyen bir operasyon ekibi üyesinin hata ayıklama (debug) mesajları arasında boğuşmasına gerek bırakmaz ve uyumluluk günlüklerini inceleyen bir denetçi yalnızca ihtiyacı olanı görür.
Düzgün günlükleme altyapısına yapılan yatırım, ona ihtiyaç duyduğunuz ilk üretim olayına kadar acil hissettirmeyen kararlardan biri. O zamana kadar geriye dönük eklemek için çok geç olur. Daha en baştan doğru inşa edin.