0

Các mẹo để ghi nhật ký hay nhất trong .NET Core

Ghi nhật ký là một khía cạnh thiết yếu của bất kỳ ứng dụng nào, đặc biệt là trong môi trường sản xuất. Ghi nhật ký cung cấp những hiểu biết quan trọng về hoạt động của ứng dụng của bạn, giúp chẩn đoán sự cố, theo dõi luồng thực thi và giám sát hiệu suất.

Trong bài đăng trên blog này, tôi sẽ chia sẻ với bạn kinh nghiệm của tôi về các phương pháp hay nhất để triển khai đăng nhập vào ứng dụng ASP.NET Core.

Mẹo 1: Sử dụng Thư viện Serilog để ghi nhật ký

ASP.NET Core có nhà cung cấp dịch vụ ghi nhật ký tích hợp sẵn - Microsoft.Extensions.Logging. Mặc dù đây là một lựa chọn tốt nhưng nó thiếu một số tính năng quan trọng.

Vì vậy, tôi khuyên bạn nên sử dụng Serilog làm thư viện ghi nhật ký rất hiệu quả và hỗ trợ ghi nhật ký có cấu trúc. Nó bổ sung thêm gói Microsoft.Extensions.Logging và bạn không cần thêm gói Serilog vào tất cả các dự án của mình.

Serilog có một hệ sinh thái “Sink” (xuất log ra các nguồn lưu trữ) rất lớn và tính linh hoạt trong cấu hình khiến nó trở thành một lựa chọn tuyệt vời để ghi nhật ký. “Sink” là nguồn nơi bạn có thể xuất và lưu trữ nhật ký của mình, nó có thể xuất nhật ký ra màn hình Console, File, cơ sở dữ liệu hoặc hệ thống giám sát.

Để bắt đầu với Serilog trong ứng dụng ASP.NET Core, hãy cài đặt các gói Nuget sau:

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

Sau đó thêm cấu hình sau vào tệp appsettings.json của bạn để định cấu hình ghi nhật ký vào Console và File:

{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File"
    ],
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Information"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      { "Name": "File", "Args": { "path": "service.log", "rollingInterval": "Day" } }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
    "Properties": {
      "Application": "ApplicationName"
    }
  }
}

Bước cuối cùng là đăng ký Serilog để hoạt động trên Microsoft Logging:

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, loggerConfig) =>
    loggerConfig.ReadFrom.Configuration(context.Configuration)
);

Bạn có thể sử dụng ILogger từ namespace Serilog hoặc bạn có thể tiếp tục sử dụng ILogger từ namespace Microsoft.Extensions.Logging và Serilog sẽ xử lý việc ghi nhật ký trong cả hai trường hợp.

Bạn có thể tìm thấy danh sách đầy đủ các “Sink” được hỗ trợ trên trang Serilog GitHub

Mẹo 2: Sử dụng mức ghi nhật ký phù hợp

Khi ghi nhật ký, bạn cần sử dụng nhiều loại cấp độ nhật ký khác nhau tùy theo mức độ quan trọng của từng thông báo:

  • Trace: nhật ký chứa các thông báo chi tiết nhất. Những tin nhắn này có thể chứa dữ liệu ứng dụng nhạy cảm. Chúng bị tắt theo mặc định và nên được sử dụng một cách tiết kiệm.
  • Debug: nhật ký được sử dụng để điều tra tương tác trong quá trình phát triển. Những tính năng này chủ yếu nên được kích hoạt trong quá trình phát triển và thử nghiệm.
  • Information: nhật ký theo dõi luồng chung của ứng dụng. Những nhật ký này phải có giá trị lâu dài.
  • Warning: nhật ký nêu bật một sự kiện không mong muốn trong luồng ứng dụng nhưng không khiến ứng dụng dừng lại.
  • Error: nhật ký đánh dấu khi luồng thực thi hiện tại bị dừng do lỗi. Những điều này sẽ chỉ ra sự thất bại trong hoạt động hoặc yêu cầu hiện tại.
  • Critical: nhật ký mô tả một ứng dụng không thể khôi phục hoặc sự cố hệ thống hoặc một lỗi nghiêm trọng cần được chú ý ngay lập tức.

Theo mặc định, trong hầu hết các ứng dụng, mức ghi nhật ký mặc định phải được đặt thành Info hoặc Warning. Đảm bảo ghi lại thông tin quan trọng nhất cho ứng dụng của bạn. Bật DebugTrace trong sản xuất khi bạn cần thêm thông tin hoặc cần điều tra bất kỳ vấn đề nào.

Mẹo 3: Sử dụng bộ lọc ghi nhật ký

Nhật ký có thể rất lớn, có thể chiếm từ GB đến TB dung lượng. Đó là lý do tại sao bạn chỉ cần ghi lại những thông tin quan trọng.

Tôi khuyên bạn nên sử dụng bộ lọc ghi nhật ký trong Serilog để kiểm soát nội dung được ghi. Với bộ lọc nhật ký, bạn có thể chỉ định mức ghi nhật ký tối thiểu cho từng logging namespace, ví dụ:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Information",
        "OpenTelemetry": "Debug",
        "Quartz": "Information",
        "Microsoft.AspNetCore.Mvc": "Warning",
        "Microsoft.AspNetCore.Routing": "Warning",
        "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
      }
    },
    "WriteTo": [
      ...
    ]
  }
}

Ở đây tôi đang đặt mức nhật ký tối thiểu cho trình ghi nhật ký asp.net core tiêu chuẩn, cho Open Telemetry và Quartz. Tất cả nhật ký có cấp độ nhật ký thấp hơn mức đã chỉ định — sẽ không được ghi lại. Ví dụ: tôi sẽ chỉ ghi nhật ký Warning, ErrorsCritical cho Microsoft.AspNetCore.Mvc trong khi nhật ký Information, DebugTrace sẽ bị bỏ qua.

Mẹo 4: Sử dụng tính năng ghi nhật ký có cấu trúc

Serilog cho phép bạn ghi nhật ký dữ liệu có cấu trúc (cặp key-value) thay vì văn bản thuần túy, giúp truy vấn và phân tích nhật ký dễ dàng hơn.

logger.LogInformation("Shipment for order '{OrderId}' is already created", request.OrderId);

logger.LogInformation("Created shipment: {@Shipment}", shipment);

Thông báo nhật ký “Shipment for order ‘{OrderId}’" bao gồm OrderId dưới dạng thuộc tính có cấu trúc. Thay vì nhúng OrderId trực tiếp vào thông điệp tường trình dưới dạng văn bản thuần túy, nó được chuyển dưới dạng tham số được đặt tên. Điều này cho phép hệ thống ghi nhật ký ghi lại OrderId dưới dạng một trường riêng biệt, có thể tìm kiếm được.

Thông báo nhật ký “Created shipment: {@Shipment}” sử dụng ký hiệu @ để tuần tự hóa đối tượng shipment thành định dạng có cấu trúc. Điều này có nghĩa là tất cả thuộc tính của đối tượng shipment được ghi lại dưới dạng các trường riêng biệt, bảo toàn cấu trúc và giúp phân tích dễ dàng hơn.

Vui lòng không bao giờ sử dụng phép nội suy chuỗi khi ghi nhật ký, nếu không bạn sẽ nhận được nhật ký văn bản thuần túy không thể tìm kiếm được bằng các tham số quan trọng:

logger.LogInformation($"Shipment for order '{request.OrderId}' is already created");

Một ví dụ khác về ghi nhật ký có cấu trúc có thể là:

logger.LogInformation("Shipment for order '{OrderId}' is already created", request.OrderId);
logger.LogInformation("Updated state of shipment {ShipmentNumber} to {NewState}", request.ShipmentNumber, request.Status);

Bằng cách triển khai ghi nhật ký theo cách có cấu trúc như vậy, bạn sẽ có thể tìm kiếm nhật ký trong công cụ xem nhật ký để nhận, ví dụ: tất cả các sự kiện liên quan đến Mã số shipment, Trạng thái hoặc OrderId nhất định.

Mẹo 5: Tránh ghi lại thông tin nhạy cảm

Đảm bảo rằng thông tin nhạy cảm như mật khẩu, số thẻ tín dụng hoặc thông tin nhận dạng cá nhân không được ghi lại. Ghi nhật ký dữ liệu nhạy cảm có thể dẫn đến lỗ hổng bảo mật.

Bạn cũng nên tránh ghi lại các thông tin bảo mật như Khóa API, mã thông báo xác thực, chuỗi kết nối, v.v.

Serilog sẵn sàng cung cấp một số tính năng và phương pháp thực hành để giúp tránh ghi lại thông tin nhạy cảm:

1. Sử dụng chính sách Destructuring:

Serilog cho phép bạn kiểm soát cách các đối tượng được ghi lại bằng chính sách Destructuring. Các chính sách này cho phép bạn dọn dẹp hoặc che giấu thông tin nhạy cảm trước khi ghi lại. Ví dụ: nếu bạn có một đối tượng phức tạp chứa dữ liệu nhạy cảm, bạn có thể xác định chính sách Destructuring để loại trừ hoặc che giấu các thuộc tính cụ thể:

Log.Logger = new LoggerConfiguration()
    .Destructure.ByMaskingProperties("Password", "CreditCardNumber")
    .WriteTo.Console()
    .CreateLogger();

var user = new
{
    Username = "anton",
    Password = "password_secret_information",
    CreditCardNumber = "1000-1000-1000-1000"
};

Log.Information("User details: {@User}", user);

Trong ví dụ này, thuộc tính PasswordCreditCardNumber được che đi trước khi được ghi.

2. Biên tập thông tin nhạy cảm theo cách thủ công:

Nếu bạn đang ghi lại từng phần thông tin riêng lẻ, bạn có thể biên tập lại hoặc dọn dẹp dữ liệu nhạy cảm theo cách thủ công trước khi chuyển nó vào bộ ghi.

var password = "password_secret_information";
var sanitizedPassword = new string('*', password.Length);

Log.Information("User attempted to login with password: {Password}", sanitizedPassword);

Ở đây, mật khẩu thực tế được thay thế bằng một chuỗi dấu hoa thị, đảm bảo rằng dữ liệu nhạy cảm không được ghi lại.

3. Định cấu hình bộ lọc để loại trừ thông tin nhạy cảm:

Serilog cho phép bạn định cấu hình các bộ lọc có thể loại trừ các sự kiện hoặc thuộc tính nhật ký cụ thể dựa trên các điều kiện nhất định. Bạn có thể thiết lập các bộ lọc để ngăn thông tin nhạy cảm bị ghi lại.

Log.Logger = new LoggerConfiguration()
    .Filter.ByExcluding(logEvent => logEvent.Properties.ContainsKey("Password"))
    .WriteTo.Console()
    .CreateLogger();

Log.Information("User details: {Username}, {Password}", "anton", "password_secret_information");

Trong ví dụ này, mọi sự kiện nhật ký chứa thuộc tính Password sẽ bị loại khỏi nhật ký.

Thường xuyên xem lại nhật ký của bạn để đảm bảo rằng không có thông tin nhạy cảm nào bị vô tình ghi lại. Triển khai kiểm tra tự động hoặc đánh giá thủ công như một phần của biện pháp bảo mật của bạn để phát hiện bất kỳ vấn đề tiềm ẩn nào.

Mẹo 6: Lỗi nhật ký

Ghi nhật ký lỗi là điều cần thiết để chẩn đoán và khắc phục sự cố trong ứng dụng. Khi xảy ra lỗi, nhật ký chi tiết có thể cung cấp thông tin chi tiết về nguyên nhân, bối cảnh và tác động của lỗi. Thông tin này rất quan trọng để giải quyết vấn đề nhanh chóng và đảm bảo độ tin cậy và ổn định cho ứng dụng của bạn.

Tùy thuộc vào yêu cầu bảo mật của ứng dụng, bạn có thể hoặc không thể ghi lại dấu vết ngoại lệ. Tuy nhiên, vui lòng không bao giờ tiết lộ stacktrace cho người dùng cuối của bạn, chẳng hạn như một phần trong phản hồi “500 Internal Server Error” của bạn.

Đây là cách bạn có thể ghi lại một ngoại lệ với stacktrace bằng Serilog

try
{
    // The code might throw an exception
}
catch (Exception ex)
{
    Log.Error(ex, "An unexpected error occurred");
}

Khi ghi lỗi, bạn có thể bao gồm thông tin ngữ cảnh có liên quan để giúp chẩn đoán sự cố. Điều này có thể bao gồm thông tin về người dùng hiện tại, chi tiết yêu cầu hoặc trạng thái của ứng dụng tại thời điểm xảy ra lỗi.

Ví dụ:

try
{
    // The code might throw an exception
}
catch (Exception ex)
{
    Log.ForContext("UserId", userId)
       .ForContext("RequestPath", requestPath)
       .Error(ex, "An error occurred while processing the request");
}

Serilog hỗ trợ các công cụ làm phong phú tùy chỉnh cho phép bạn tự động thêm các phần thông tin cụ thể vào tất cả các sự kiện trong nhật ký, bao gồm cả lỗi. Điều này có thể đảm bảo rằng thông tin ngữ cảnh quan trọng luôn được đưa vào nhật ký lỗi.

Log.Logger = new LoggerConfiguration()
    .Enrich.WithProperty("ApplicationName", "ApplicationName")
    .Enrich.WithProperty("Environment", "Production")
    .WriteTo.Console()
    .CreateLogger();

try
{
    // The code might throw an exception
}
catch (Exception ex)
{
    Log.Error(ex, "An error occurred");
}

Mẹo 7: Giám sát kích thước và hiệu suất nhật ký

Việc ghi nhật ký nếu không được quản lý đúng cách có thể gây tắc nghẽn hiệu suất và tiêu tốn quá nhiều dung lượng lưu trữ.

Bạn có thể giới hạn kích thước nhật ký bằng cách áp dụng các kỹ thuật sau:

  • sử dụng mức ghi nhật ký thích hợp
  • sử dụng bộ lọc ghi nhật ký
  • triển khai luân chuyển và lưu giữ nhật ký để ghi nhật ký trên File

Chúng ta đã nói về việc sử dụng các mức ghi nhật ký và bộ lọc ghi nhật ký thích hợp. Hãy cùng khám phá việc luân chuyển và lưu giữ nhật ký để ghi nhật ký trên File.

Xoay vòng nhật ký bao gồm việc tự động lưu trữ và tạo tệp nhật ký mới theo các khoảng thời gian được chỉ định, chẳng hạn như hàng ngày hoặc hàng tuần. Chính sách lưu giữ nhật ký xác định thời gian lưu giữ nhật ký trước khi chúng bị xóa. Cả hai cách thực hành này đều giúp quản lý việc sử dụng ổ đĩa bằng cách ngăn chặn các tệp nhật ký phát triển vô thời hạn.

Ví dụ:

Log.Logger = new LoggerConfiguration()
    .WriteTo.File(
        "logs/service.log",
        rollingInterval: RollingInterval.Day, // Rotate logs daily
        retainedFileCountLimit: 7)  // Retain only the last 7 days of logs
    .CreateLogger();

Cấu hình này đảm bảo rằng các tệp nhật ký được luân chuyển hàng ngày và chỉ giữ lại nhật ký trong 7 ngày qua, ngăn không cho các nhật ký cũ tiêu tốn quá nhiều dung lượng ổ đĩa.

Bạn cũng có thể định cấu hình điều này trong appsettings.json:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information"
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "logs/service.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 7, // Retain logs for 7 days
          "fileSizeLimitBytes": 10485760 // Limit file size to 10MB
        }
      }
    ]
  }
}

Việc ghi nhật ký có thể ảnh hưởng đến hiệu suất ứng dụng, đặc biệt nếu nhật ký được ghi vào đĩa hoặc gửi qua mạng. Giám sát chi phí do quá trình ghi nhật ký gây ra, đặc biệt là trong các ứng dụng có lưu lượng truy cập cao hoặc hiệu suất quan trọng. Hãy cân nhắc sử dụng tính năng ghi nhật ký không đồng bộ để giảm thiểu tác động đến hiệu suất ứng dụng.

Log.Logger = new LoggerConfiguration()
    .WriteTo.Async(a => a.File("logs/log.txt", rollingInterval: RollingInterval.Day))
    .CreateLogger();

Ở đây tôi sử dụng Serilog.Sinks.Async - một trình bao bọc không đồng bộ cho các “Sink” Serilog ghi nhật ký trên một luồng nền. Nó có thể hữu ích cho việc ghi nhật ký tệp có thể bị ảnh hưởng bởi tắc nghẽn I/O.

Việc tắt ghi nhật ký console trong môi trường sản xuất của bạn cũng rất quan trọng. Ghi nhật ký console có thể làm chậm đáng kể ứng dụng của bạn.

Mẹo 8: Tập trung và trực quan hóa nhật ký bằng giao diện người dùng ghi nhật ký

Trong các ứng dụng hiện đại, đặc biệt là các ứng dụng chạy trong môi trường phân tán (distributed) hoặc kiến trúc vi dịch vụ (microservices), việc ghi nhật ký có thể nhanh chóng trở nên quá tải. Nhật ký thường được trải rộng trên nhiều máy chủ, dịch vụ hoặc vùng chứa (containers), gây khó khăn cho việc hiểu rõ hơn về tình trạng chung của hệ thống hoặc khắc phục sự cố cụ thể. Giao diện người dùng ghi nhật ký tập trung, như Seq, giải quyết những thách thức này bằng cách tổng hợp nhật ký từ nhiều nguồn khác nhau vào một giao diện duy nhất, có thể tìm kiếm, cung cấp các công cụ phân tích và trực quan hóa mạnh mẽ.

Việc sử dụng giao diện người dùng ghi nhật ký như Seq không chỉ tập trung nhật ký của bạn mà còn nâng cao khả năng giám sát, tìm kiếm và phân tích dữ liệu nhật ký trong thời gian thực.

Bạn có nhớ tôi đã nói với bạn về việc ghi nhật ký có cấu trúc không? Bạn có thể tìm kiếm các tham số nhật ký cụ thể bằng Seq hoặc bất kỳ công cụ tương tự nào khác.

Một ưu điểm khác của việc sử dụng các công cụ tập trung để quản lý nhật ký - là có thể định cấu hình cảnh báo về lỗi hoặc các nhật ký quan trọng khác.

Để bắt đầu với Seq, bạn cần thêm gói Nuget sau:

dotnet add package Serilog.Sinks.Seq

Và cập nhật cấu hình ghi nhật ký Serilog trong appsettings.json:

{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.Seq"
    ],
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Information"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://localhost:5341"
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
    "Properties": {
      "Application": "ShippingService"
    }
  }
}

Tại đây, chúng tôi định cấu hình ghi nhật ký vào Console (không sử dụng trong sản xuất) và Seq. Chúng tôi trỏ URL của Seq tới http://localhost:5341 khi chạy cục bộ. Khi chạy dịch vụ bên trong vùng chứa docker - bạn cần sử dụng tên của vùng chứa docker thay vì localhost: http://seq:5341.

Đây là cách ghi nhật ký trong Seq:

Nếu bạn muốn tìm hiểu Cách triển khai tính năng ghi nhật ký có cấu trúc và theo dõi phân tán cho vi dịch vụ bằng Seq, hãy nhớ xem bài đăng trên blog của tôi.

Đây là danh sách các lựa chọn thay thế Seq, trong trường hợp bạn cần thứ khác:

  • ELK Stack: Elasticsearch, Logstash, and Kibana
  • Datadog
  • New Relic
  • Loggly
  • GrayLog
  • Azure Monitor Logs
  • Amazon CloudWatch Logs (AWS) Hy vọng bạn thấy bài viết blog này hữu ích. Happy coding!

Nguồn: Medium


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí