Chapter 04

Exception & Logging
trong Thực Tế

Production setup, Correlation ID, MediatR integration, Error Pages

Full Production Setup

Một file Program.cs hoàn chỉnh với mọi thứ kết hợp — Serilog, Exception Handler, ProblemDetails, Health Checks:

C#
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// ═══ LOGGING ═══
builder.Host.UseSerilog((ctx, cfg) =>
    cfg
        .ReadFrom.Configuration(ctx.Configuration)
        .WriteTo.Console(
            outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
        .WriteTo.File("logs/app-.log",
            rollingInterval: RollingInterval.Day,
            retainedFileCountLimit: 30)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
        .Enrich.WithEnvironmentName()
);

// ═══ EXCEPTION HANDLING ═══
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails(opt =>
{
    opt.CustomizeProblemDetails = ctx =>
    {
        ctx.ProblemDetails.Extensions["traceId"] =
            ctx.HttpContext.TraceIdentifier;
    };
});

// ═══ SERVICES ═══
builder.Services.AddControllers();
builder.Services.AddHealthChecks();

var app = builder.Build();

// ═══ MIDDLEWARE PIPELINE (thứ tự quan trọng!) ═══
app.UseExceptionHandler();       // 1. Bắt mọi exception
app.UseSerilogRequestLogging();  // 2. Log HTTP request/response
app.UseRouting();                // 3. Routing
app.UseAuthentication();         // 4. AuthN
app.UseAuthorization();          // 5. AuthZ
app.MapControllers();            // 6. Endpoints
app.MapHealthChecks("/health");  // 7. Health check

app.Run();
UseSerilogRequestLogging() tự động log mỗi HTTP request với method, path, status code, và thời gian xử lý. Thay thế verbose logging mặc định của ASP.NET — gọn gàng và hữu ích hơn nhiều.

Request Correlation

Khi debug production, bạn cần tìm tất cả log liên quan đến một request. Correlation ID giải quyết vấn đề này:

Client
X-Correlation-Id
Middleware
push to LogContext
Service A
log with CorrelationId
Service B
log with CorrelationId
C#
public class CorrelationIdMiddleware
{
    private const string Header = "X-Correlation-Id";
    private readonly RequestDelegate _next;

    public CorrelationIdMiddleware(RequestDelegate next)
        => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        // Lấy từ header hoặc tạo mới
        var correlationId = context.Request.Headers[Header]
            .FirstOrDefault() ?? Guid.NewGuid().ToString();

        // Thêm vào response header
        context.Response.Headers[Header] = correlationId;

        // Push vào Serilog LogContext — mọi log trong request này có CorrelationId
        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            await _next(context);
        }
    }
}

// Đăng ký SAU ExceptionHandler, TRƯỚC Routing:
// app.UseExceptionHandler();
// app.UseMiddleware<CorrelationIdMiddleware>();
// app.UseSerilogRequestLogging();
Giờ bạn có thể query trong Seq: CorrelationId = "abc-123" — thấy toàn bộ log từ đầu đến cuối request, qua mọi service, mọi handler.

Kết hợp với MediatR

MediatR Pipeline Behaviors là nơi lý tưởng để thêm logging và exception handling ở tầng application:

C#
// UnhandledExceptionBehavior — bắt exception trong handler, log rồi re-throw
public class UnhandledExceptionBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly ILogger<UnhandledExceptionBehavior<TRequest, TResponse>> _logger;

    public UnhandledExceptionBehavior(
        ILogger<UnhandledExceptionBehavior<TRequest, TResponse>> logger)
        => _logger = logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        try
        {
            return await next();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Unhandled exception for {RequestName}: {@Request}",
                typeof(TRequest).Name, request);
            throw; // Re-throw — GlobalExceptionHandler xử lý tiếp
        }
    }
}
Behavior này log request data kèm exception — thông tin mà GlobalExceptionHandler không có. Kết hợp cả hai: Behavior log context, GlobalExceptionHandler trả ProblemDetails. Xem thêm: MediatR Pipeline Behaviors.

Error Pages: Development vs Production

🚧

Development

UseDeveloperExceptionPage()
  • Hiển thị stack trace đầy đủ
  • Source code dòng gây lỗi
  • Query string, headers, cookies
  • Routing information
  • KHÔNG BAO GIỜ dùng trong production!
🛡

Production

UseExceptionHandler() + ProblemDetails
  • User thấy error message thân thiện
  • Chi tiết lỗi chỉ nằm trong log
  • ProblemDetails JSON cho API
  • Custom error page cho MVC
  • Bảo mật: không lộ internal info
C#
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage(); // Stack trace đầy đủ
}
else
{
    app.UseExceptionHandler();       // ProblemDetails cho API
    app.UseHsts();                   // HTTP Strict Transport Security
}
DeveloperExceptionPage lộ: source code, connection strings, config values, internal paths. Attacker dùng thông tin này để tấn công. LUÔN kiểm tra ASPNETCORE_ENVIRONMENT trên production server.

Health Checks

Endpoint /health cho load balancer và monitoring tools kiểm tra app còn sống không:

C#
// Đăng ký health checks
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>()  // Kiểm tra DB connection
    .AddCheck("redis", () =>
    {
        // Custom health check
        try { redis.Ping(); return HealthCheckResult.Healthy(); }
        catch { return HealthCheckResult.Unhealthy("Redis down"); }
    });

// Map endpoint
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});