CMS & Content Platforms

Serilog Logging Cấu Trúc trong Optimizely CMS 12 | Hướng dẫn .NET 8

By Ginbok7 min read

Giới thiệu

Hướng dẫn này trình bày cách triển khai Serilog - một thư viện ghi nhật ký cấu trúc (structured logging) mạnh mẽ - trong Optimizely CMS 12.

Những gì bạn sẽ học:

Điều kiện tiên quyết:


Tại sao nên dùng Serilog?

✅ Ghi nhật ký Cấu trúc (Structured Logging) - Nhật ký là dữ liệu có cấu trúc, không chỉ là văn bản thuần
✅ Nhiều Đầu ra (Multiple Outputs) - Console, files, cơ sở dữ liệu đồng thời
✅ Hiệu suất Cao (High Performance) - Ghi nhật ký bất đồng bộ
✅ Cấu hình Dễ dàng (Easy Configuration) - Cài đặt dựa trên JSON


Bước 1: Cài đặt Packages

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Bước 2: Khởi tạo trong Program.cs

using Serilog;

public class Program
{
    public static void Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build())
            .CreateLogger();

        try
        {
            Log.Information("Starting Optimizely CMS");
            CreateHostBuilder(args).Build().Run();
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Application start-up failed");
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog()
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Bước 3: Cấu hình appsettings.json

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-log-.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "fileSizeLimitBytes": 10485760,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
          "restrictedToMinimumLevel": "Error"
        }
      },
      {
        "Name": "Console",
        "Args": {
          "restrictedToMinimumLevel": "Information"
        }
      }
    ]
  }
}

Cài đặt Chính:

Cài đặt (Setting) Giá trị (Value) Mô tả (Description)
rollingInterval Day Tạo file mới mỗi ngày
retainedFileCountLimit 30 Giữ lại trong 30 ngày
fileSizeLimitBytes 10485760 (10MB) Kích thước file tối đa
File Level Error Chỉ ghi lỗi vào file
Console Level Information Ghi tất cả thông tin vào console

Bước 4: Sử dụng trong Mã nguồn

Ghi nhật ký Cơ bản

public class ProductController : Controller
{
    private readonly ILogger<ProductController> _logger;

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

    public IActionResult Index(int productId)
    {
        _logger.LogInformation("Loading product {ProductId}", productId);

        try
        {
            var product = LoadProduct(productId);
            return View(product);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load product {ProductId}", productId);
            return NotFound();
        }
    }
}

Ghi nhật ký Cấu trúc

// ❌ KHÔNG TỐT: String interpolation
_logger.LogInformation($"User {userId} created order {orderId}");

// ✅ TỐT: Thuộc tính cấu trúc
_logger.LogInformation(
    "User {UserId} created order {OrderId} with total {Total:C}",
    userId, orderId, total);

Các Cấp độ Log

_logger.LogTrace("Detailed tracing");        // Chỉ dành cho Phát triển
_logger.LogDebug("Debug information");       // Phát triển
_logger.LogInformation("Normal events");     // Sản xuất
_logger.LogWarning("Potential issues");      // Sản xuất
_logger.LogError(ex, "Errors");              // Sản xuất
_logger.LogCritical("Critical failures");    // Sản xuất

Xem Nhật ký

Tùy chọn 1: Console (Môi trường Phát triển)

Khi chạy dotnet run, tất cả nhật ký sẽ hiển thị theo thời gian thực trong console.

Ưu điểm: ✅ Thời gian thực, ✅ Tất cả cấp độ
Nhược điểm: ❌ Không lưu trữ dai dẳng

Tùy chọn 2: Log Files (Môi trường Sản xuất)

Files được lưu tại: /logs/app-log-20260120.txt

Ưu điểm: ✅ Lưu trữ dai dẳng, ✅ Chỉ ghi lỗi
Nhược điểm: ❌ Không theo thời gian thực

Tùy chọn 3: Công cụ Quản trị Tùy chỉnh

Tạo một trình xem nhật ký đơn giản trong trang quản trị CMS:

[Authorize(Roles = "CmsAdmins")]
public class LogViewerController : Controller
{
    private readonly IWebHostEnvironment _environment;

    public LogViewerController(IWebHostEnvironment environment)
    {
        _environment = environment;
    }

    [Route("admin/logs")]
    public IActionResult Index()
    {
        var logDir = Path.Combine(_environment.ContentRootPath, "logs");
        var files = Directory.GetFiles(logDir, "*.txt")
            .Select(f => new FileInfo(f))
            .OrderByDescending(f => f.LastWriteTime)
            .Select(f => new {
                Name = f.Name,
                Size = $"{(f.Length / 1024)} KB",
                Modified = f.LastWriteTime
            });

        return View(files);
    }

    [Route("admin/logs/download/{fileName}")]
    public IActionResult Download(string fileName)
    {
        var logDir = Path.Combine(_environment.ContentRootPath, "logs");
        var filePath = Path.Combine(logDir, fileName);
        
        if (!System.IO.File.Exists(filePath))
            return NotFound();

        var content = System.IO.File.ReadAllBytes(filePath);
        return File(content, "text/plain", fileName);
    }
}

Các Phương pháp Hay nhất

1. Sử dụng Ghi nhật ký Cấu trúc

// ✅ Luôn sử dụng các thuộc tính có tên
_logger.LogInformation("Processing {ItemCount} items", items.Count);

2. Không bao giờ ghi lại Dữ liệu Nhạy cảm

Không bao giờ ghi lại:

3. Chọn Cấp độ Thích hợp

4. Sử dụng Ghi nhật ký Có điều kiện

if (_logger.IsEnabled(LogLevel.Debug))
{
    var expensiveData = ComputeExpensiveData();
    _logger.LogDebug("Debug: {Data}", expensiveData);
}

5. Thêm Ngữ cảnh với Scopes

using (_logger.BeginScope("OrderId={OrderId}", orderId))
{
    _logger.LogInformation("Validating order");
    _logger.LogInformation("Processing payment");
    // Tất cả nhật ký sẽ bao gồm OrderId
}

Các Vấn đề Thường gặp

Vấn đề 1: Không có Nhật ký trong Files

Sự cố: Console hiển thị nhật ký nhưng các file trống

Giải pháp:

// Kiểm tra restrictedToMinimumLevel không quá cao
{
  "Args": {
    "restrictedToMinimumLevel": "Information"  // Giảm mức này
  }
}

Vấn đề 2: Kích thước File quá lớn

Sự cố: Thư mục log chiếm quá nhiều dung lượng đĩa

Giải pháp:

{
  "Serilog": {
    "WriteTo": [{
      "Args": {
        "rollingInterval": "Day",
        "retainedFileCountLimit": 7,        // Chỉ giữ 7 ngày
        "fileSizeLimitBytes": 5242880,      // Tối đa 5MB
        "restrictedToMinimumLevel": "Warning"  // Chỉ cảnh báo trở lên
      }
    }]
  }
}

Vấn đề 3: Công cụ Quản trị không hiển thị Files

Sự cố: Đường dẫn không khớp giữa Serilog và công cụ quản trị

Giải pháp:

// Đảm bảo đường dẫn khớp nhau
{
  "Serilog": {
    "WriteTo": [{
      "Args": {
        "path": "logs/app-log-.txt"  // ← Phải khớp với đường dẫn công cụ
      }
    }]
  }
}

Xác minh trong code:

var logPath = Path.Combine(_environment.ContentRootPath, "logs");
if (!Directory.Exists(logPath))
{
    _logger.LogWarning("Log directory not found: {Path}", logPath);
}

Vấn đề 4: Ảnh hưởng đến Hiệu suất

Sự cố: Ứng dụng chạy chậm do việc ghi nhật ký

Giải pháp:

// 1. Giới hạn cấp độ log trong môi trường sản xuất
"MinimumLevel": { "Default": "Information" }

// 2. Sử dụng sink file bất đồng bộ (async file sink)
services.AddSerilog(config => 
    config.WriteTo.Async(a => a.File("logs/app.txt")));

// 3. Ghi nhật ký có điều kiện cho các hoạt động tốn kém
if (_logger.IsEnabled(LogLevel.Debug))
{
    _logger.LogDebug("Expensive: {Data}", ComputeData());
}

Cấu hình Cụ thể theo Môi trường

appsettings.Development.json:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Verbose"
    }
  }
}

appsettings.Production.json:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Warning"
    }
  }
}

Tham khảo Nhanh

Hướng dẫn Cấp độ Log

Cấp độ (Level) Khi nào (When) Ví dụ (Example)
Trace Chi tiết theo dõi Cache lookup
Debug Sự kiện nội bộ Query results
Information Luồng bình thường Order created
Warning Vấn đề tiềm ẩn Slow response
Error Ngoại lệ Payment failed
Critical Ứng dụng sập Database down

Các Mẫu thường dùng

// Ghi nhật ký đơn giản
_logger.LogInformation("User logged in");

// Với thuộc tính
_logger.LogInformation("Order {OrderId} total {Total:C}", id, total);

// Với ngoại lệ
_logger.LogError(ex, "Failed to process {OrderId}", id);

// Với scope
using (_logger.BeginScope("UserId={UserId}", userId))
{
    // Tất cả nhật ký bao gồm UserId
}

// Có điều kiện
if (_logger.IsEnabled(LogLevel.Debug))
{
    _logger.LogDebug("Data: {Data}", expensiveData);
}

 

Kết luận

Serilog trong Optimizely CMS 12 cung cấp:

✅ Ghi nhật ký cấu trúc để phân tích tốt hơn
✅ Nhiều đầu ra (console + files)
✅ Cấu hình linh hoạt thông qua JSON
✅ Sẵn sàng cho sản xuất với tính năng xoay file (file rotation)

Các điểm chính cần nhớ

  1. Khởi tạo Serilog trước khi ứng dụng khởi động
  2. Sử dụng ghi nhật ký cấu trúc với các thuộc tính có tên
  3. Không bao giờ ghi lại dữ liệu nhạy cảm
  4. Đặt cấp độ log thích hợp cho từng môi trường
  5. Triển khai chính sách lưu giữ để quản lý dung lượng đĩa
#Serilog OptimizelyCMS DotNet Logging Backend
← Back to Articles