# 淺談 .NET 預設 Logger 及其優化技巧
## 紀錄等級
以下是根據[在 C# 和 .NET 中進行日誌記錄](https://learn.microsoft.com/zh-tw/dotnet/core/extensions/logging?tabs=command-line#log-level)所定義的各種等級及建議使用時機。其中 `None` 值最高,主要用於在 Filter 設定中完全停用所有 LOG。
| 等級 | 值 | 方法 | 描述 |
|------|----|----|------|
| 追蹤 (Trace) | 0 | LogTrace | 包含最詳細的訊息。這些訊息可能包含敏感性應用程式資料。這些訊息預設為停用,且不應在生產環境中啟用。 |
| 偵錯 (Debug) | 1 | LogDebug | 用於偵錯和開發。由於用量大,在生產環境中請小心使用。 |
| 資訊 (Information) | 2 | LogInformation | 追蹤一般應用程式流程。可能具有長期價值。 |
| 警告 (Warning) | 3 | LogWarning | 用於處理異常或意外事件。通常包含不會導致應用程式失敗的錯誤或狀況。 |
| 錯誤 (Error) | 4 | LogError | 無法處理的錯誤和例外情況。這些訊息指出目前作業或要求中發生失敗,而不是整個應用程式的失敗。 |
| 重要 (Critical) | 5 | LogCritical | 需要立即處理的故障。範例:資料遺失情況、磁碟空間不足。 |
| None | 6 | 無 | 指定不應該寫入任何訊息。 |
## 基本注入設定
`WebApplication.CreateBuilder()` 在建立時會自動設定預設的 Logger。查看 .NET 原始碼可以發現,在 [HostingHostBuilderExtensions.cs](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs) 的 `AddDefaultServices()` 方法中,系統會自動設定基本的 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 日誌框架首選的主要原因之一。
###### tags: `.NET` `.NET Core & .NET 5+` `ASP.NET Core` `Logging`