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:
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:
// 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" } } }
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)
- 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)
- 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ể
// 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
{UserId} thay vì
{userId}. Đây là convention giúp nhất quán khi query log trong Seq hoặc Elasticsearch.
Logging Providers
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).
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.
dotnet add package Serilog.AspNetCore
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() );
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.
// 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%'
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:
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 }
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.