日誌系統 Logging
為什麼需要日誌(Logging)?
日誌就是你的程式在執行過程中留下的紀錄,讓你知道發生了什麼事。
💡 比喻:飛機的黑盒子
- 飛機正常飛行時,黑盒子持續記錄各種數據
- 一旦發生事故,調查員靠黑盒子還原當時的狀況
- 日誌就是你程式的「黑盒子」
- 系統上線後出了問題,你沒辦法用 breakpoint 除錯
- 你只能靠日誌來還原問題發生的經過
沒有日誌時:
使用者:「你的系統剛剛壞了!」
工程師:「什麼時候?發生了什麼?」
使用者:「不知道,反正就是壞了。」
工程師:「……」(無從查起)
有日誌時:
使用者:「你的系統剛剛壞了!」
工程師:(打開日誌)
[2024-01-15 14:30:22] ERROR: 資料庫連線逾時,連線字串:Server=db01
[2024-01-15 14:30:22] ERROR: 重試 3 次後仍然失敗
[2024-01-15 14:30:23] CRITICAL: 訂單服務無法處理請求
工程師:「找到了!是資料庫 db01 掛了。」
ILogger:ASP.NET Core 內建日誌
// ASP.NET Core 已經內建日誌功能,不需要額外安裝套件
// 透過 DI 注入 ILogger<T> 就能使用
using Microsoft.Extensions.Logging;
public class OrderController : ControllerBase
{
// 注入 ILogger,泛型參數用目前的類別名稱
// 這樣日誌會自動標記是哪個類別產生的
private readonly ILogger<OrderController> _logger;
// 建構式注入
public OrderController(ILogger<OrderController> logger)
{
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(OrderRequest request)
{
// 記錄一般資訊
_logger.LogInformation("開始建立訂單,使用者:{UserId}", request.UserId);
try
{
// 處理訂單邏輯...
var order = await _orderService.CreateAsync(request);
// 記錄成功訊息
_logger.LogInformation("訂單建立成功,訂單編號:{OrderId}", order.Id);
return Ok(order);
}
catch (Exception ex)
{
// 記錄錯誤訊息(包含例外物件)
_logger.LogError(ex, "建立訂單失敗,使用者:{UserId}", request.UserId);
return StatusCode(500, "系統錯誤,請稍後再試");
}
}
}
Log Levels:日誌等級
日誌等級由低到高(越高越嚴重):
等級 數值 用途 比喻
──────────────────────────────────────────────────────
Trace 0 最詳細的追蹤資訊 偵探的隨身筆記(每個細節)
Debug 1 開發除錯用的資訊 工程師的草稿紙
Information 2 一般流程記錄 航海日誌(正常航行紀錄)
Warning 3 不正常但還能運作 黃燈警告(注意但不停車)
Error 4 發生錯誤,某個操作失敗 紅燈(出事了!)
Critical 5 系統即將崩潰的嚴重錯誤 火災警報(快逃!)
設定某個等級後,只有「等於或高於」該等級的日誌才會被記錄。
例如設定 Warning,則 Warning、Error、Critical 會被記錄,
Trace、Debug、Information 會被忽略。
// 各種日誌等級的使用範例
public class PaymentService
{
// 注入日誌服務
private readonly ILogger<PaymentService> _logger;
public PaymentService(ILogger<PaymentService> logger)
{
_logger = logger;
}
public async Task ProcessPaymentAsync(PaymentRequest request)
{
// Trace:非常詳細的追蹤資訊(通常只在本機開發時開啟)
_logger.LogTrace("進入 ProcessPaymentAsync,參數:{@Request}", request);
// Debug:開發除錯用
_logger.LogDebug("開始驗證支付金額:{Amount}", request.Amount);
// Information:一般業務流程紀錄
_logger.LogInformation("處理付款,訂單:{OrderId},金額:{Amount}",
request.OrderId, request.Amount);
// Warning:不正常但系統還能運作
if (request.Amount > 100000)
{
_logger.LogWarning("大額交易警告!訂單:{OrderId},金額:{Amount}",
request.OrderId, request.Amount);
}
try
{
// 呼叫金流 API...
await CallPaymentGateway(request);
}
catch (TimeoutException ex)
{
// Error:操作失敗
_logger.LogError(ex, "付款閘道逾時,訂單:{OrderId}", request.OrderId);
throw;
}
catch (Exception ex)
{
// Critical:系統層級的嚴重錯誤
_logger.LogCritical(ex, "付款系統完全無法使用!");
throw;
}
}
}
結構化日誌(Structured Logging)
傳統日誌 vs 結構化日誌:
傳統日誌(純文字):
"2024-01-15 使用者 123 購買了產品 456,金額 999 元"
→ 要搜尋「使用者 123 的所有訂單」很困難(只能用字串搜尋)
結構化日誌(有欄位):
{
"Timestamp": "2024-01-15",
"Message": "使用者購買產品",
"UserId": 123,
"ProductId": 456,
"Amount": 999
}
→ 可以直接查詢 WHERE UserId = 123(像查資料庫一樣!)
// ASP.NET Core 的 ILogger 天生支援結構化日誌
// ❌ 錯誤:用字串拼接(無法被結構化解析)
_logger.LogInformation("使用者 " + userId + " 購買了產品 " + productId);
// 也不要用 $"..." 字串插值
_logger.LogInformation($"使用者 {userId} 購買了產品 {productId}");
// ✅ 正確:用佔位符(Placeholder),讓日誌框架結構化處理
// {UserId} 和 {ProductId} 會被當作獨立的欄位存儲
_logger.LogInformation("使用者 {UserId} 購買了產品 {ProductId}",
userId, productId);
// 這樣在日誌查詢平台(如 Seq、Kibana)就能:
// - WHERE UserId = 123
// - GROUP BY ProductId
// - 對 Amount 做統計分析
// 記錄完整物件:用 @ 前綴做解構
var order = new { Id = 1, Total = 999, Items = 3 };
// @ 前綴會把物件序列化成 JSON 記錄
_logger.LogInformation("訂單資訊:{@Order}", order);
// 輸出:訂單資訊:{ Id: 1, Total: 999, Items: 3 }
Serilog:更強大的日誌套件
// 1. 安裝 Serilog 套件
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.Console
// dotnet add package Serilog.Sinks.File
// 2. 在 Program.cs 設定 Serilog
using Serilog;
// 設定 Serilog 日誌管線
Log.Logger = new LoggerConfiguration()
// 最低記錄等級
.MinimumLevel.Information()
// 覆寫特定命名空間的等級(減少微軟框架的雜訊)
.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
// 輸出到 Console(有顏色標示)
.WriteTo.Console()
// 輸出到檔案(每天一個檔案,保留 30 天)
.WriteTo.File("logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30)
// 建立 Logger
.CreateLogger();
// 把 Serilog 設定為 ASP.NET Core 的日誌提供者
builder.Host.UseSerilog();
// 3. 使用方式跟 ILogger 完全一樣!
// 因為 Serilog 實作了 ILogger 介面
// 你的 Controller 和 Service 不用改任何程式碼
日誌輸出目標(Sinks)
// Serilog 可以同時輸出到多個目標
Log.Logger = new LoggerConfiguration()
// 輸出到 Console(開發時方便看)
.WriteTo.Console(
// 自訂輸出格式
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
)
// 輸出到檔案(正式環境基本需求)
.WriteTo.File("logs/app-.log",
// 每天換一個新檔案
rollingInterval: RollingInterval.Day,
// 單一檔案最大 10MB
fileSizeLimitBytes: 10_000_000,
// 超過大小就建新檔
rollOnFileSizeLimit: true,
// 保留最近 30 個檔案
retainedFileCountLimit: 30
)
// 輸出到 Seq(結構化日誌查詢平台)
// .WriteTo.Seq("http://localhost:5341")
.CreateLogger();
appsettings.json 設定日誌等級
// appsettings.json - 透過設定檔控制日誌等級
{
// 日誌設定區塊
"Logging": {
"LogLevel": {
// 預設等級:Information(記錄一般資訊以上)
"Default": "Information",
// 微軟框架的日誌等級設高一點(減少雜訊)
"Microsoft.AspNetCore": "Warning",
// EF Core 的 SQL 查詢日誌(開發時可以打開看 SQL)
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
// Serilog 專用設定
"Serilog": {
"MinimumLevel": {
// 預設最低等級
"Default": "Information",
// 覆寫特定命名空間
"Override": {
// 微軟框架只記錄 Warning 以上
"Microsoft": "Warning",
// EF Core 只記錄 Warning 以上
"Microsoft.EntityFrameworkCore": "Warning"
}
}
}
}
// appsettings.Development.json - 開發環境可以開更多日誌
{
// 開發環境的日誌設定
"Logging": {
"LogLevel": {
// 開發時記錄更詳細的資訊
"Default": "Debug",
// 可以看到 EF Core 產生的 SQL
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:在日誌中記錄敏感資料
// ❌ 錯誤:把密碼、信用卡號寫進日誌
_logger.LogInformation("使用者登入,帳號:{Email},密碼:{Password}",
email, password);
// 日誌可能存在檔案、傳到遠端伺服器,任何看到日誌的人都能看到密碼!
// ❌ 錯誤:記錄完整的信用卡號
_logger.LogInformation("付款成功,卡號:{CardNumber}", cardNumber);
// ✅ 正確:永遠不要記錄敏感資料
_logger.LogInformation("使用者登入,帳號:{Email}", email);
// 信用卡只記錄後四碼
_logger.LogInformation("付款成功,卡號尾碼:{CardLast4}",
cardNumber[^4..]);
❌ 錯誤 2:使用錯誤的日誌等級
// ❌ 錯誤:所有東西都用 Information
_logger.LogInformation("系統即將崩潰!記憶體不足!"); // 這應該是 Critical!
_logger.LogInformation("找不到使用者 123"); // 這可能是 Warning
_logger.LogInformation("變數 x 的值是 42"); // 這應該是 Debug
// ✅ 正確:根據嚴重程度選擇適當的等級
_logger.LogCritical("系統即將崩潰!記憶體不足!"); // 最嚴重的錯誤
_logger.LogWarning("找不到使用者 {UserId}", 123); // 不正常但不致命
_logger.LogDebug("變數 x 的值是 {Value}", 42); // 除錯用的資訊
❌ 錯誤 3:不用結構化日誌
// ❌ 錯誤:用字串拼接或插值
_logger.LogInformation($"使用者 {userId} 在 {DateTime.Now} 購買了 {productName}");
// 問題 1:無法在日誌平台做結構化查詢
// 問題 2:效能差(即使日誌等級設定會跳過這條,字串還是會被拼接)
// ✅ 正確:用訊息模板(Message Template)
_logger.LogInformation(
"使用者 {UserId} 在 {PurchaseTime} 購買了 {ProductName}",
userId, DateTime.Now, productName);
// 每個佔位符都是可查詢的結構化欄位
// 如果日誌等級設定會跳過,佔位符不會被處理(效能更好)
💡 重點整理
| 概念 | 說明 |
|---|---|
| ILogger |
ASP.NET Core 內建的日誌介面 |
| Log Levels | Trace < Debug < Information < Warning < Error < Critical |
| 結構化日誌 | 用佔位符而非字串拼接,讓日誌可以被查詢分析 |
| Serilog | 強大的第三方日誌套件,支援多種輸出目標 |
| Sinks | 日誌的輸出目標:Console、File、Seq、Elasticsearch 等 |
| appsettings.json | 透過設定檔控制不同命名空間的日誌等級 |