筆記目錄

Skip to content

淺談 .NET 預設 Logger 及其優化技巧

紀錄等級

以下是根據 在 C# 和 .NET 中進行日誌記錄 所定義的各種等級及建議使用時機。其中 None 值最高,主要用於在 Filter 設定中完全停用所有 LOG。

等級方法描述
追蹤 (Trace)0LogTrace包含最詳細的訊息。這些訊息可能包含敏感性應用程式資料。這些訊息預設為停用,且不應在生產環境中啟用。
偵錯 (Debug)1LogDebug用於偵錯和開發。由於用量大,在生產環境中請小心使用。
資訊 (Information)2LogInformation追蹤一般應用程式流程。可能具有長期價值。
警告 (Warning)3LogWarning用於處理異常或意外事件。通常包含不會導致應用程式失敗的錯誤或狀況。
錯誤 (Error)4LogError無法處理的錯誤和例外情況。這些訊息指出目前作業或要求中發生失敗,而不是整個應用程式的失敗。
重要 (Critical)5LogCritical需要立即處理的故障。範例:資料遺失情況、磁碟空間不足。
None6指定不應該寫入任何訊息。

基本注入設定

WebApplication.CreateBuilder() 在建立時會自動設定預設的 Logger。查看 .NET 原始碼可以發現,在 HostingHostBuilderExtensions.csAddDefaultServices() 方法中,系統會自動設定基本的 Logger Provider:

csharp
// 來自 .NET 原始碼
services.AddLogging(logging => {
    bool isWindows =
#if NET
        OperatingSystem.IsWindows();
#elif NETFRAMEWORK
        Environment.OSVersion.Platform == PlatformID.Win32NT;
#else
        RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif

    // IMPORTANT: This needs to be added *before* configuration is loaded, this lets
    // the defaults be overridden by the configuration.
    if (isWindows) {
        // Default the EventLogLoggerProvider to warning or above
        logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
    }

    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
#if NET
    if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi())
#endif
    {
        logging.AddConsole();
    }
    logging.AddDebug();
    logging.AddEventSourceLogger();

    if (isWindows) {
        // Add the EventLogLoggerProvider on windows machines
        logging.AddEventLog();
    }

    logging.Configure(options => {
        options.ActivityTrackingOptions =
            ActivityTrackingOptions.SpanId |
            ActivityTrackingOptions.TraceId |
            ActivityTrackingOptions.ParentId;
    });
});

這表示預設情況下,WebApplication.CreateBuilder() 已經為我們設定了下列 Logger Provider:

  • 從 Configuration 的 "Logging" 區段讀取設定。
  • Console Logger (輸出到控制台)
  • Debug Logger (輸出到偵錯工具)
  • EventSource Logger (用於 EventSource 追蹤)

實際使用時,我們可以透過以下方式自訂 Logger 設定:

csharp
// CreateBuilder 本身就會有預設設定
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// 可以在 DI 方式調整增加的 LoggerProvider
builder.Services.AddLogging(logging => {
    logging.ClearProviders(); // 清除預設的 Provider
    logging.AddConsole();     // 加入 Console Provider
    logging.AddDebug();       // 加入 Debug Provider
});

// 也可以直接簡化成如下
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();

程式注入 Logger 的方式

在 ASP.NET Core 中,Logger 通常透過依賴注入方式使用。以下是一個簡化的範例:

csharp
namespace LoggerTest.Controllers {
    [ApiController]
    [Route("api/[controller]")]
    public class UserController : ControllerBase {
        private readonly ILogger<UserController> logger;

        public UserController(ILogger<UserController> logger) {
            this.logger = logger;
            this.logger.LogInformation("UserController 已初始化");
        }
    }
}

使用 Appsettings.json 控制日誌等級

我們可以透過 appsettings.json 檔案來設定不同元件的日誌等級。以下是一個特殊情境的範例,使用多種類型的 Logger:

csharp
namespace LoggerTest.Controllers {
    [ApiController]
    [Route("api/[controller]")]
    public class TestController : Controller {
        public TestController(ILogger<TestService1> logger1, ILogger<TestService2> logger2) {
            logger1.LogInformation("TestController 已成功初始化並注入 Service1 的 Logger。");
            logger2.LogInformation("TestController 已成功初始化並注入 Service2 的 Logger。");
        }
    }
}

namespace LoggerTest.Services {
    public class TestService1 {
        public TestService1() {
        }

        public void TestMethod() {
            // Do something
        }
    }

    public class TestService2 {
        public TestService2() {
        }

        public void TestMethod() {
            // Do something
        }
    }
}

配合以下的 appsettings.json 設定:

json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "LoggerTest.Services.TestService1": "Warning"
    },
    "Debug": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.Hosting": "Trace"
      }
    },
    "EventSource": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "AllowedHosts": "*"
}

在這個設定檔中:

  • 外層的 "LogLevel" 設定適用於所有 Logger Provider。
  • "Debug" 和 "EventSource" 區段中的設定只適用於指定的 Provider。
  • "Default" 表示預設的最低日誌等級。
  • 特定命名空間如 "Microsoft.AspNetCore" 可以設定自己的日誌等級。

使用上述設定執行程式後,會發現只有 Service2 的日誌會被記錄,因為 Service1 的日誌等級被設為 Warning。

bash
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7109
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5164
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Programming\Projects\LoggerTest\LoggerTest
info: LoggerTest.Services.TestService2[0]
      TestController 已成功初始化並注入 Service2 Logger。

使用萬用字元進行設定

如果想要針對多個命名空間套用相同設定,可以使用萬用字元 *。例如,"*.Services": "Warning" 會將所有以 .Services 結尾的命名空間日誌等級設為 Warning。

程式碼中的進階過濾設定

若需要更精細的控制,可以在程式碼中使用 Filter 進行設定:

csharp
// 根據命名空間過濾
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);

// 根據提供者類型過濾
builder.Logging.AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Warning);
builder.Logging.AddFilter<DebugLoggerProvider>("Microsoft.AspNetCore.Mvc", LogLevel.Debug);

// 使用萬用字元
builder.Logging.AddFilter("Microsoft.*", LogLevel.Warning);
builder.Logging.AddFilter("*.Repository", LogLevel.Debug);

// 針對特定類別的日誌器
builder.Logging.AddFilter(typeof(Program).FullName, LogLevel.Debug);

// 使用更精細的控制 - 使用函數型過濾器
builder.Logging.AddFilter((provider, category, logLevel) => {
    // 在工作日顯示更詳細的日誌
    if (DateTime.Now.DayOfWeek != DayOfWeek.Saturday
        && DateTime.Now.DayOfWeek != DayOfWeek.Sunday) {
        return logLevel >= LogLevel.Debug;
    }

    // 週末只顯示警告及以上等級的日誌
    return logLevel >= LogLevel.Warning;
});

使用 Logger.IsEnabled 提高效能

在需要執行昂貴操作來產生日誌訊息的情況下,我們可以先檢查特定日誌等級是否已啟用,以避免不必要的運算:

csharp
if (logger.IsEnabled(LogLevel.Information)) {
    // 這裡可能會執行較耗資源的操作,例如從資料庫查詢或複雜計算
    int processedRecords = await database.GetProcessedRecordsCount();
    logger.LogInformation("系統已完成資料更新,共處理 {Count} 筆資料。", processedRecords);
}

這樣做的好處是,當設定檔將日誌等級設為 Warning 或更高時,我們可以完全跳過這些耗資源的操作。

結構化日誌的優勢與使用

結構化日誌的主要好處

  1. 效能優勢:結構化日誌最大的優勢在於避免不必要的字串串接操作。Logger 只會在實際需要寫入日誌時才執行字串組合,這就是所謂的「遞延執行」。當日誌等級設定為不記錄某些層級時,相關的字串處理完全不會執行,從而提升應用程式效能。

  2. 資料整合便利性:生成的日誌可以輕鬆被 Elasticsearch、Kibana、Logstash (ELK) 等工具解析,便於集中式日誌管理。

結構化日誌的使用方式

csharp
// Visual Studio 和 .NET 分析器會透過 CA2254 警告提醒避免以下寫法
logger.LogInformation(
    "使用者 " + user.Id + " 已登入系統,所屬部門: " + user.Department
);

// 正確的結構化日誌寫法
logger.LogInformation(
    "使用者 {UserId} 已登入系統,所屬部門: {Department}",
    user.Id,
    user.Department
);

大括號的特殊用法

如果你需要在日誌訊息中包含大括號字元 {},需要使用雙大括號 來進行轉義。例如:

csharp
// 如果想輸出含有大括號的文字,需要用雙大括號
logger.LogInformation("這是一個 JSON 範例:{{\"name\": \"value\"}}");

// 輸出結果為:這是一個 JSON 範例:{"name": "value"}

// 注意:{XXX} 中的 XXX 是作為變數佔位符使用,與傳入的參數位置相關
// 不是根據參數名稱對應,而是根據參數順序
logger.LogInformation("名稱: {Name}, 年齡: {Age}", "小明", 25);
// 輸出:名稱: 小明, 年齡: 25

使用 JSON 格式輸出

透過將 AddConsole() 替換成 AddJsonConsole(),我們可以獲得結構化的 JSON 輸出:

csharp
builder.Logging.AddConsole();
// 改為
builder.Logging.AddJsonConsole();

產生的 JSON 日誌格式如下(為了閱讀方便,有進行排版):

json
{
    "EventId": 0,
    "LogLevel": "Information",
    "Category": "LoggerTest.Controllers.TestController",
    "Message": "使用者 U12345 已登入系統,所屬部門: 研發部",
    "State": {
        "UserId": "U12345",
        "Department": "研發部",
        "{OriginalFormat}": "使用者 {UserId} 已登入系統,所屬部門: {Department}"
    },
    "Timestamp": "2025-03-23T15:30:00.123Z"
}

這種結構化的 JSON 格式特別適合與 ELK 等日誌分析系統整合,讓我們能更有效地搜尋、過濾和分析日誌。這也是為什麼近年來 Serilog 逐漸取代 NLog 成為 .NET 日誌框架首選的主要原因之一。

異動歷程

  • 2025-03-23 初版文件建立。