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 });