Chapter 03

Logging System
trong ASP.NET Core

ILogger, Log Levels, Structured Logging, Serilog — ghi log đúng cách

ILogger<T>

ASP.NET Core có sẵn hệ thống logging tích hợp. Inject ILogger<T> vào bất kỳ class nào:

C#
public class ProductService
{
    private readonly ILogger<ProductService> _logger;
    private readonly AppDbContext _db;

    public ProductService(
        ILogger<ProductService> logger,
        AppDbContext db)
    {
        _logger = logger;
        _db = db;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        _logger.LogInformation(
            "Getting product {ProductId}", id);

        var product = await _db.Products.FindAsync(id);

        if (product == null)
        {
            _logger.LogWarning(
                "Product {ProductId} not found", id);
            throw new NotFoundException("Product", id);
        }

        _logger.LogDebug(
            "Product {ProductId} found: {ProductName}",
            id, product.Name);

        return product;
    }
}
ILogger<T> tự động đặt category = tên đầy đủ của class T. Ví dụ: ILogger<ProductService> có category = "MyApp.Services.ProductService". Category giúp lọc log theo class trong config.

Log Levels

6 mức độ nghiêm trọng, từ thấp đến cao. Khi set minimum level, tất cả level thấp hơn bị bỏ qua:

Critical App crash, mất dữ liệu, cần xử lý ngay lập tức 5
Error Lỗi request cụ thể, unhandled exception, operation failed 4
Warning Bất thường nhưng app vẫn chạy, vấn đề tiềm ẩn 3
Information Luồng chạy bình thường, request started/completed 2
Debug Thông tin chi tiết cho developer, biến trung gian 1
Trace Chi tiết nhất, framework internals, rất verbose 0
JSON
// appsettings.json — Development
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",         // Mọi thứ từ Debug trở lên
      "Microsoft.AspNetCore": "Warning" // Framework chỉ Warning+
    }
  }
}

// appsettings.Production.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",    // Production: Info trở lên
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Warning"
    }
  }
}
ĐỪNG dùng Trace/Debug trong Production. Output cực kỳ verbose — hàng ngàn dòng log mỗi giây, đầy disk, giảm performance. Production nên bắt đầu từ Information.

Structured Logging

Đây là sự khác biệt quan trọng nhất giữa logging "đúng" và "sai" trong .NET:

String Interpolation (SAI)

$"User {userId} logged in"
  • Mất thông tin structured data
  • Không query được theo property
  • Performance kém (allocate string)
  • Không filter được theo userId cụ thể

Message Template (ĐÚNG)

"User {UserId} logged in", userId
  • UserId là property riêng biệt
  • Query: WHERE UserId = 42
  • Performance tốt (không allocate nếu level tắt)
  • Tìm tất cả log của user cụ thể
C#
// SAI — string interpolation
_logger.LogInformation($"User {userId} bought product {productId} for {price}");
// Output: "User 42 bought product 7 for 99.99"
// → Chỉ là một string, không query được

// ĐÚNG — message template
_logger.LogInformation(
    "User {UserId} bought product {ProductId} for {Price}",
    userId, productId, price);
// Output structured:
// {
//   "Message": "User 42 bought product 7 for 99.99",
//   "UserId": 42,
//   "ProductId": 7,
//   "Price": 99.99
// }
// → Query: SELECT * FROM logs WHERE UserId = 42
Message template dùng PascalCase cho tên property: {UserId} thay vì {userId}. Đây là convention giúp nhất quán khi query log trong Seq hoặc Elasticsearch.

Logging Providers

Console & Debug Built-in +

Mặc định có sẵn. Console ghi ra stdout, Debug ghi ra System.Diagnostics.Debug. Phù hợp cho development, KHÔNG đủ cho production (không persist, không query).

Serilog Third-party +

Thư viện logging phổ biến nhất cho .NET. Hỗ trợ hàng chục "sinks" (đích ghi): Console, File, Seq, Elasticsearch, Application Insights, v.v.

Bash
dotnet add package Serilog.AspNetCore
C#
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((ctx, cfg) =>
    cfg
        .ReadFrom.Configuration(ctx.Configuration)
        .WriteTo.Console()
        .WriteTo.File("logs/app-.log",
            rollingInterval: RollingInterval.Day,
            retainedFileCountLimit: 30)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
);
Seq Log Server +

Structured log server với UI query mạnh mẽ. Miễn phí cho single-user. Tương thích hoàn hảo với Serilog. Query structured properties trực quan.

C#
// Thêm Seq sink
cfg.WriteTo.Seq("http://localhost:5341");

// Giờ bạn có thể query:
// UserId = 42 AND ProductId = 7
// @Level = 'Error' AND Exception like '%Timeout%'
Application Insights Azure +

Monitoring toàn diện của Azure. Tích hợp sẵn distributed tracing, performance metrics, failure analysis. Phù hợp cho ứng dụng trên Azure.

Log Scopes

Scopes thêm context chung cho tất cả log entries trong một block code. Hữu ích để group log theo request hoặc operation:

C#
public async Task ProcessOrderAsync(int orderId, int userId)
{
    using (_logger.BeginScope(
        new Dictionary<string, object>
        {
            ["OrderId"] = orderId,
            ["UserId"] = userId
        }))
    {
        _logger.LogInformation("Processing order");
        // Output: {"Message":"Processing order", "OrderId":123, "UserId":42}

        await ValidateOrder(orderId);
        // Mọi log trong ValidateOrder() cũng có OrderId và UserId!

        await ChargePayment(orderId);
        _logger.LogInformation("Order completed");
        // Output: {"Message":"Order completed", "OrderId":123, "UserId":42}
    }
    // Scope kết thúc — OrderId, UserId không còn trong log
}
Serilog tự động bao gồm scope properties khi dùng Enrich.FromLogContext(). Đặc biệt hữu ích khi cần trace tất cả log liên quan đến một request hoặc transaction.