← Tüm yazılara dön

Structured Logging — Serilog + Seq Kurulumu

Structured Logging — Serilog + Seq Kurulumu

Structured Logging — Serilog + Seq Kurulumu

Düz Metin Log Neden Yetersiz?

Production'da bir hata alındı. Logları açtınız:

[2024-01-15 14:32:11] ERROR: Sipariş işlenemedi
[2024-01-15 14:32:11] INFO: Kullanıcı giriş yaptı
[2024-01-15 14:32:12] ERROR: Veritabanı bağlantısı kesildi
[2024-01-15 14:32:12] INFO: Sipariş oluşturuldu
[2024-01-15 14:32:13] ERROR: Sipariş işlenemedi

Sorular:

  • Hangi kullanıcının siparişi işlenemedi?
  • Hangi sipariş ID'si?
  • Kaç kullanıcı etkilendi?
  • Hata ne sıklıkla tekrarlanıyor?
  • Veritabanı bağlantısı mı yoksa başka bir şey mi neden oldu?

Düz metin logda bu soruların cevabı yoktur. Aynı log yapılandırılmış biçimde yazılsaydı:

{
  "Timestamp": "2024-01-15T14:32:11.234Z",
  "Level": "Error",
  "Message": "Sipariş işlenemedi",
  "OrderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "CustomerId": "8a7b6c5d-4e3f-2a1b-0c9d-8e7f6a5b4c3d",
  "TotalAmount": 1250.00,
  "ErrorCode": "PAYMENT_GATEWAY_TIMEOUT",
  "TraceId": "abc123def456",
  "RequestId": "req_789xyz"
}

Artık her sorunun cevabı var. OrderId'ye göre filtreleyebilir, CustomerId bazında gruplayabilir, ErrorCode'a göre sayabilirsiniz.

İşte bu structured logging'dir.


Serilog Nedir?

Serilog, .NET için en yaygın kullanılan structured logging kütüphanesidir. İki temel kavramla çalışır:

Sink — Log nereye yazılacak? (Console, dosya, Seq, Elasticsearch...)
Enricher — Log'a ne ekleneceği? (MachineName, ThreadId, CorrelationId...)

Uygulama
   ↓ log yazar
Serilog
   ├── Sink: Console
   ├── Sink: File (günlük rotate)
   ├── Sink: Seq (merkezi platform)
   └── Sink: Application Insights

Seq Nedir?

Seq, structured log'lar için özel geliştirilmiş merkezi bir log yönetim platformudur. Elasticsearch/Kibana kurulumu gerektirmeden, MSSQL gibi bilinen bir teknoloji üzerinde çalışır. Development için ücretsizdir.

Serilog → Seq
             ├── Full-text arama
             ├── LINQ benzeri filtre sorguları
             ├── Alert ve notification
             ├── Dashboard ve grafik
             └── Log retention yönetimi

Kurulum

NuGet Paketleri

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process
dotnet add package Serilog.Enrichers.CorrelationId

Seq Docker ile Ayağa Kaldırma

# docker-compose.yml
version: '3.8'

services:
  seq:
    image: datalust/seq:latest
    container_name: seq
    ports:
      - "5341:5341"    # Ingestion port — Serilog buraya yazar
      - "8080:80"      # Web UI — http://localhost:8080
    environment:
      ACCEPT_EULA: Y
      SEQ_FIRSTRUN_ADMINPASSWORD: "Admin123!"
    volumes:
      - seq_data:/data

volumes:
  seq_data:
docker-compose up -d
# Web UI: http://localhost:8080
# Kullanıcı: admin / Admin123!

Temel Yapılandırma

Program.cs — Kod ile Yapılandırma

// Program.cs
using Serilog;
using Serilog.Events;

// Bootstrap logger — uygulama başlarken oluşabilecek hataları yakala
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .WriteTo.Console()
    .CreateBootstrapLogger();

try
{
    Log.Information("Uygulama başlatılıyor...");

    var builder = WebApplication.CreateBuilder(args);

    // Serilog'u host'a entegre et
    builder.Host.UseSerilog((context, services, configuration) =>
    {
        configuration
            .ReadFrom.Configuration(context.Configuration)
            .ReadFrom.Services(services)
            .Enrich.FromLogContext()
            .Enrich.WithMachineName()
            .Enrich.WithEnvironmentName()
            .Enrich.WithThreadId()
            .Enrich.WithProcessId()
            .Enrich.WithCorrelationId()
            .WriteTo.Console(
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
                                "{Properties:j}{NewLine}{Exception}")
            .WriteTo.File(
                path: "logs/app-.log",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] " +
                                "{Message:lj}{NewLine}{Exception}")
            .WriteTo.Seq(context.Configuration["Seq:ServerUrl"]!);
    });

    // Servis kayıtları
    builder.Services.AddControllers();
    // ...

    var app = builder.Build();

    // HTTP istek logları — Serilog middleware
    app.UseSerilogRequestLogging(options =>
    {
        options.MessageTemplate =
            "HTTP {RequestMethod} {RequestPath} → {StatusCode} ({Elapsed:0.0000}ms)";

        // İstek loglarına ek bilgi ekle
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
            diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
            diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent);
            diagnosticContext.Set("ClientIp",
                httpContext.Connection.RemoteIpAddress?.ToString());
            diagnosticContext.Set("UserId",
                httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "anonymous");
        };

        // Health check loglarını bastır
        options.GetLevel = (httpContext, elapsed, ex) =>
        {
            if (ex is not null) return LogEventLevel.Error;
            if (httpContext.Response.StatusCode >= 500) return LogEventLevel.Error;
            if (httpContext.Response.StatusCode >= 400) return LogEventLevel.Warning;
            if (httpContext.Request.Path.StartsWithSegments("/health")) return LogEventLevel.Verbose;
            if (elapsed > 1000) return LogEventLevel.Warning;
            return LogEventLevel.Information;
        };
    });

    app.MapControllers();
    app.Run();

    return 0;
}
catch (Exception ex)
{
    Log.Fatal(ex, "Uygulama beklenmedik şekilde sonlandı.");
    return 1;
}
finally
{
    // Buffer'daki tüm logları flush et
    await Log.CloseAndFlushAsync();
}

appsettings.json ile Yapılandırma

Kod yerine configuration dosyasında yönetmek deployment'ı kolaylaştırır — kod değiştirmeden log level değiştirilebilir:

{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File",
      "Serilog.Sinks.Seq"
    ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning",
        "Microsoft.EntityFrameworkCore.Database.Command": "Information",
        "System": "Warning",
        "System.Net.Http.HttpClient": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "fileSizeLimitBytes": 104857600,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://localhost:5341",
          "apiKey": ""
        }
      }
    ],
    "Enrich": [
      "FromLogContext",
      "WithMachineName",
      "WithEnvironmentName",
      "WithThreadId",
      "WithProcessId"
    ],
    "Properties": {
      "Application": "MyApp",
      "Environment": "Production"
    }
  },
  "Seq": {
    "ServerUrl": "http://localhost:5341"
  }
}

Structured Logging Doğru Kullanımı

Message Template — En Önemli Kural

// ❌ String interpolation — yapıyı bozar, değerler ayrıştırılamaz
_logger.LogInformation($"Sipariş {orderId} oluşturuldu, tutar: {amount}");

// ❌ String concatenation — aynı sorun
_logger.LogInformation("Sipariş " + orderId + " oluşturuldu");

// ✅ Message template — değerler ayrı property olarak saklanır
_logger.LogInformation(
    "Sipariş oluşturuldu. OrderId: {OrderId} | Tutar: {Amount:C}",
    orderId, amount);

// ✅ Nesne destructuring — @ prefix ile tüm property'ler ayrı ayrı indexlenir
_logger.LogInformation("Sipariş alındı: {@Order}", new
{
    OrderId = order.Id,
    CustomerId = order.CustomerId,
    ItemCount = order.Items.Count,
    TotalAmount = order.TotalAmount
});

Fark nedir?

String interpolation ile Seq'te arama yapamazsınız — metin içinde kaybolur.
Message template ile OrderId = "3fa85f64" olarak ayrı bir field oluşur, filtrelenebilir.

Log Level Seçimi

public class OrderService
{
    private readonly ILogger<OrderService> _logger;

    // Verbose — sadece deep debugging, production'da kapalı
    _logger.LogTrace("GetOrders çağrıldı. CustomerId: {CustomerId}", customerId);

    // Debug — geliştirme ve test için
    _logger.LogDebug(
        "Cache miss. OrderId: {OrderId} — veritabanından okunuyor.", orderId);

    // Information — iş akışının normal adımları
    _logger.LogInformation(
        "Sipariş oluşturuldu. OrderId: {OrderId} | CustomerId: {CustomerId} | Tutar: {Amount:C}",
        order.Id, order.CustomerId, order.TotalAmount);

    // Warning — beklenen ama dikkate değer durumlar
    _logger.LogWarning(
        "Yetersiz stok. ProductId: {ProductId} | İstenen: {Requested} | Mevcut: {Available}",
        productId, requestedQty, availableQty);

    // Error — işlenmiş hata — uygulama devam ediyor
    _logger.LogError(ex,
        "Ödeme işlemi başarısız. OrderId: {OrderId} | Hata: {ErrorCode}",
        orderId, ex.ErrorCode);

    // Fatal — uygulama devam edemiyor
    _logger.LogCritical(ex,
        "Veritabanına bağlanılamıyor. ConnectionString: {Server}",
        serverName);
}

LogContext ile Dinamik Enrichment

// Handler veya middleware'de — tüm alt log'lara otomatik eklenir
public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(
        CreateOrderCommand command,
        CancellationToken ct)
    {
        // Bu using bloğu içindeki tüm log'lara OrderId eklenir
        using (LogContext.PushProperty("OrderId", command.OrderId))
        using (LogContext.PushProperty("CustomerId", command.CustomerId))
        using (LogContext.PushProperty("Operation", "CreateOrder"))
        {
            _logger.LogInformation("Sipariş işlemi başladı.");

            var order = await CreateOrderAsync(command, ct);

            _logger.LogInformation(
                "Ödeme başlatılıyor. Amount: {Amount}", order.TotalAmount);

            await ProcessPaymentAsync(order, ct);

            _logger.LogInformation("Sipariş başarıyla tamamlandı.");
            // Her log satırında OrderId, CustomerId ve Operation otomatik var

            return order.Id;
        }
    }
}

Enricher'lar

Correlation ID Enricher

Dağıtık sistemlerde bir isteği uçtan uca izlemek için:

// CorrelationIdMiddleware.cs
public class CorrelationIdEnricherMiddleware
{
    private readonly RequestDelegate _next;

    public CorrelationIdEnricherMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers["X-Correlation-Id"]
            .FirstOrDefault() ?? Guid.NewGuid().ToString("N")[..12];

        context.Response.Headers["X-Correlation-Id"] = correlationId;

        using (LogContext.PushProperty("CorrelationId", correlationId))
        using (LogContext.PushProperty("RequestPath", context.Request.Path.Value))
        using (LogContext.PushProperty("RequestMethod", context.Request.Method))
        {
            await _next(context);
        }
    }
}

Custom Enricher — Tenant Bilgisi

// TenantEnricher.cs
public class TenantLogEnricher : ILogEventEnricher
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantLogEnricher(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        if (httpContext is null) return;

        // Tenant bilgisini her log'a ekle
        if (httpContext.Items.TryGetValue("TenantId", out var tenantId))
        {
            logEvent.AddPropertyIfAbsent(
                propertyFactory.CreateProperty("TenantId", tenantId));
        }

        // Kullanıcı bilgisi
        var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (userId is not null)
        {
            logEvent.AddPropertyIfAbsent(
                propertyFactory.CreateProperty("UserId", userId));
        }

        var userEmail = httpContext.User.FindFirst(ClaimTypes.Email)?.Value;
        if (userEmail is not null)
        {
            logEvent.AddPropertyIfAbsent(
                propertyFactory.CreateProperty("UserEmail", userEmail));
        }
    }
}

// Kayıt
builder.Services.AddSingleton<TenantLogEnricher>();

builder.Host.UseSerilog((ctx, services, cfg) =>
{
    cfg.Enrich.With(services.GetRequiredService<TenantLogEnricher>());
    // ...
});

Sink'ler

Ortam Bazlı Sink Yapılandırması

builder.Host.UseSerilog((context, services, configuration) =>
{
    var env = context.HostingEnvironment;

    configuration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName();

    // Development — renkli console, verbose
    if (env.IsDevelopment())
    {
        configuration
            .MinimumLevel.Debug()
            .WriteTo.Console(
                theme: AnsiConsoleTheme.Code,
                outputTemplate:
                    "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}{NewLine}" +
                    "  {Message:lj}{NewLine}" +
                    "  {Properties:j}{NewLine}" +
                    "{Exception}");
    }

    // Staging — console + Seq
    if (env.IsStaging())
    {
        configuration
            .MinimumLevel.Information()
            .WriteTo.Console()
            .WriteTo.Seq(context.Configuration["Seq:ServerUrl"]!);
    }

    // Production — dosya + Seq + hata alert'i
    if (env.IsProduction())
    {
        configuration
            .MinimumLevel.Warning()
            .WriteTo.File(
                new JsonFormatter(),                // JSON formatında dosyaya yaz
                path: "logs/app-.json",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 14)
            .WriteTo.Seq(
                serverUrl: context.Configuration["Seq:ServerUrl"]!,
                apiKey: context.Configuration["Seq:ApiKey"],
                restrictedToMinimumLevel: LogEventLevel.Information)
            .WriteTo.Seq(
                serverUrl: context.Configuration["Seq:ServerUrl"]!,
                apiKey: context.Configuration["Seq:ApiKey"],
                restrictedToMinimumLevel: LogEventLevel.Error);
                // Error ve üzeri için alert kurulabilir
    }
});

Email Sink — Kritik Hatalar İçin

dotnet add package Serilog.Sinks.Email
configuration.WriteTo.Email(
    new EmailConnectionInfo
    {
        FromEmail = "noreply@myapp.com",
        ToEmail = "ops@myapp.com",
        MailServer = "smtp.myapp.com",
        Port = 587,
        EnableSsl = true,
        NetworkCredentials = new NetworkCredential("user", "pass"),
        EmailSubject = "[MyApp] Kritik Hata Oluştu"
    },
    outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}",
    restrictedToMinimumLevel: LogEventLevel.Fatal,
    batchPostingLimit: 1);

Seq'te Sorgulama

Seq, LINQ benzeri kendi sorgu dilini sunar:

-- Tüm error ve üzeri
@Level = 'Error' or @Level = 'Fatal'

-- Belirli sipariş
OrderId = '3fa85f64-5717-4562-b3fc-2c963f66afa6'

-- Belirli kullanıcının son 1 saatteki hataları
UserId = 'user-123' and @Level = 'Error' and @Timestamp > Now() - 1h

-- 500ms'den uzun süren istekler
Elapsed > 500

-- Belirli hata kodu
ErrorCode = 'PAYMENT_GATEWAY_TIMEOUT'

-- Tenant bazlı sorgulama
TenantId = 'tenant-abc' and @Level >= 'Warning'

-- Regex ile arama
@Message like '%veritabanı%'

-- Son 5 dakikada hata sayısı
select count(*) from stream
where @Level = 'Error'
  and @Timestamp > Now() - 5m

Alert Yapılandırması

Seq'te belirli koşullar sağlandığında bildirim gönderebilirsiniz:

Seq UI → Alerts → Add Alert

Alert 1 — Fatal Error:
  Signal: @Level = 'Fatal'
  Trigger: İlk oluştuğunda
  Notification: Email → ops@myapp.com

Alert 2 — Yüksek Error Oranı:
  Signal: @Level = 'Error'
  Trigger: 5 dakikada 10'dan fazla
  Notification: Webhook → Slack

Alert 3 — Yavaş İstekler:
  Signal: Elapsed > 2000
  Trigger: 1 dakikada 5'ten fazla
  Notification: Email

Performance Logging — Stopwatch Pattern

// PerformanceLogger.cs
public class PerformanceLogger<T>
{
    private readonly ILogger<T> _logger;

    public PerformanceLogger(ILogger<T> logger)
    {
        _logger = logger;
    }

    public async Task<TResult> MeasureAsync<TResult>(
        string operationName,
        Func<Task<TResult>> operation,
        int warningThresholdMs = 500,
        int errorThresholdMs = 2000)
    {
        var stopwatch = Stopwatch.StartNew();

        try
        {
            var result = await operation();
            stopwatch.Stop();

            var level = stopwatch.ElapsedMilliseconds switch
            {
                var ms when ms > errorThresholdMs => LogEventLevel.Error,
                var ms when ms > warningThresholdMs => LogEventLevel.Warning,
                _ => LogEventLevel.Debug
            };

            _logger.Log(
                (LogLevel)(int)level,
                "Operasyon tamamlandı. {OperationName} → {ElapsedMs}ms",
                operationName,
                stopwatch.ElapsedMilliseconds);

            return result;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            _logger.LogError(ex,
                "Operasyon başarısız. {OperationName} → {ElapsedMs}ms",
                operationName,
                stopwatch.ElapsedMilliseconds);

            throw;
        }
    }
}

// Kullanım
public class ProductService
{
    private readonly PerformanceLogger<ProductService> _perfLogger;

    public async Task<List<ProductDto>> GetAllAsync()
    {
        return await _perfLogger.MeasureAsync(
            "GetAllProducts",
            async () => await _context.Products
                .AsNoTracking()
                .Select(p => new ProductDto(p.Id, p.Name, p.Price))
                .ToListAsync(),
            warningThresholdMs: 200);
    }
}

MediatR Pipeline ile Log

// LoggingPipelineBehavior.cs
public class LoggingPipelineBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingPipelineBehavior<TRequest, TResponse>> _logger;

    public LoggingPipelineBehavior(
        ILogger<LoggingPipelineBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        var requestName = typeof(TRequest).Name;
        var requestId = Guid.NewGuid().ToString("N")[..8];

        using (LogContext.PushProperty("RequestName", requestName))
        using (LogContext.PushProperty("RequestId", requestId))
        {
            _logger.LogInformation(
                "MediatR isteği başladı. {RequestName} | Parametreler: {@Request}",
                requestName, request);

            var stopwatch = Stopwatch.StartNew();

            try
            {
                var response = await next();
                stopwatch.Stop();

                _logger.LogInformation(
                    "MediatR isteği tamamlandı. {RequestName} → {ElapsedMs}ms",
                    requestName, stopwatch.ElapsedMilliseconds);

                return response;
            }
            catch (Exception ex)
            {
                stopwatch.Stop();

                _logger.LogError(ex,
                    "MediatR isteği başarısız. {RequestName} → {ElapsedMs}ms",
                    requestName, stopwatch.ElapsedMilliseconds);

                throw;
            }
        }
    }
}

EF Core Sorgu Logları

// appsettings.Development.json
{
  "Serilog": {
    "MinimumLevel": {
      "Override": {
        "Microsoft.EntityFrameworkCore.Database.Command": "Information"
      }
    }
  }
}
// DbContext'te slow query detection
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(connectionString)
        .LogTo(
            action: (eventId, level) => level == LogLevel.Warning,
            logger: message => Log.Warning("EF Core Yavaş Sorgu: {Message}", message))
        .EnableSensitiveDataLogging(isDevelopment)
        .EnableDetailedErrors(isDevelopment)
        .ConfigureWarnings(warnings =>
            warnings.Throw(RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning));
}

Log Sanitization — Hassas Veri Maskeleme

// Hassas verileri loglamadan önce maskele
public class SensitiveDataDestructuringPolicy : IDestructuringPolicy
{
    private static readonly HashSet<string> SensitiveFields =
    [
        "Password", "CreditCardNumber", "Cvv",
        "TaxNumber", "BankAccount", "ApiKey", "Secret"
    ];

    public bool TryDestructure(
        object value,
        ILogEventPropertyValueFactory propertyValueFactory,
        out LogEventPropertyValue result)
    {
        if (value is not string strValue ||
            !SensitiveFields.Any(f =>
                strValue.Contains(f, StringComparison.OrdinalIgnoreCase)))
        {
            result = null!;
            return false;
        }

        result = new ScalarValue("***MASKED***");
        return true;
    }
}

// Kayıt
configuration.Destructure.With<SensitiveDataDestructuringPolicy>();

// Attribute ile property bazlı maskeleme
public class CreateUserRequest
{
    public string Email { get; set; } = string.Empty;

    [LogMasked(ShowFirst = 0, ShowLast = 0, PreserveLength = false)]
    public string Password { get; set; } = string.Empty;

    [LogMasked(ShowFirst = 4, ShowLast = 4)]
    public string CreditCardNumber { get; set; } = string.Empty;
    // 1234 **** **** 5678
}

Production Checklist

✅ String interpolation yerine message template kullanılıyor
✅ Her ortam için ayrı minimum log level tanımlandı
✅ Microsoft ve System namespace'leri Warning seviyesinde
✅ Health check endpoint'leri Verbose seviyesinde — noise azaltıldı
✅ 500ms üzeri istekler Warning olarak loglanıyor
✅ CorrelationId her log satırında mevcut
✅ Hassas veriler (şifre, kart no) loglanmıyor veya maskeleniyor
✅ Seq'te alert tanımlandı — Fatal ve yüksek Error oranı için
✅ Log dosyaları günlük rotate ediliyor, 14-30 gün saklanıyor
✅ Log buffer flush edilmiş — Log.CloseAndFlushAsync() çağrılıyor
✅ Seq API key ile korunuyor
✅ LogContext.PushProperty ile request bazlı enrichment yapılıyor
✅ EF Core sorgu logları sadece development'ta açık
✅ Production'da minimum level Warning veya Information
✅ JSON formatter ile dosyaya yazılıyor — makine tarafından okunabilir

Temel Çıkarımlar

  • Düz metin log arama yapılabilir değildir — structured logging zorunludur
  • Message template kullanmak Serilog'un temel kuralıdır — interpolation yapıyı bozar
  • Enricher'lar her log satırına otomatik bağlam ekler — manuel yazmaya gerek kalmaz
  • LogContext.PushProperty ile request bazlı bilgi tüm alt log'lara yayılır
  • Seq olmadan structured logging'in değeri yarıya düşer — sorgu yapılamazsa ne anlamı var?
  • Hassas veriler asla loglanmamalıdır — maskeleme zorunludur
  • Log level seçimi önemlidir — her şeyi Information yazmak gürültü yaratır
  • Alert mekanizması olmadan monitoring eksiktir — hata olduğunda haberdar olmalısınız

Log yazmak değil, doğru log yazmak önemlidir. Yapılandırılmamış, aranabilir olmayan log; karanlıkta el yordamıyla ilerlemektir. Structured logging ise production'da ışığı açmaktır.