跳至主要内容

Serilog 結構化日誌指南

用途:為 .NET 應用程式提供結構化日誌(Structured Logging)能力

NuGetSerilog(核心)/ Serilog.Sinks.Console / Serilog.Sinks.File

授權:Apache-2.0

支援:.NET 6+、.NET Framework 4.6.2+


為什麼需要結構化日誌

傳統日誌把所有資訊塞進一個字串,想找特定設備的溫度異常?只能 grep 整段文字,遇到格式不統一就沒轍:

// ❌ 傳統文字日誌:資訊埋在字串裡,無法程式化搜尋
logger.Info($"[{DateTime.Now}] Device CMP-01 temperature 85.3°C exceeded threshold");
logger.Info($"[{DateTime.Now}] Device CVD-02 temperature 72.1°C normal");

想搜尋「溫度超過 80°C 的所有設備」?得寫正規表達式解析字串,而且每次 log 格式稍有變動就要重寫。

結構化日誌 的核心思想是:日誌事件不只是一行文字,而是一個帶有具名屬性的結構化資料

// ✅ 結構化日誌:屬性獨立保存,可搜尋、可分析
Log.Information("Device {DeviceId} temperature {Temperature}°C {Status}",
"CMP-01", 85.3, "exceeded threshold");

底層儲存的不只是字串,而是:

{
"MessageTemplate": "Device {DeviceId} temperature {Temperature}°C {Status}",
"DeviceId": "CMP-01",
"Temperature": 85.3,
"Status": "exceeded threshold",
"Timestamp": "2026-04-10T14:30:00.000+08:00",
"Level": "Information"
}

這樣就能用 SQL 或搜尋引擎直接查詢:WHERE Temperature > 80 AND DeviceId LIKE 'CMP%'

在工業自動化場景的價值

場景傳統日誌的痛點結構化日誌的優勢
設備通訊異常追蹤grep 搜尋 IP、Port 混在文字中直接查 PlcId = "PLC-03"
效能瓶頸分析手動計算回應時間散佈在各行數值欄位可直接統計 avg/p99
跨工站關聯分析不同格式難以 join共用 StationId 屬性直接關聯
審計合規(FDA Part 11)文字日誌容易被篡改結構化寫入 DB 可加簽章

Serilog vs NLog vs Microsoft.Extensions.Logging

比較項目SerilogNLogMicrosoft.Extensions.Logging (MEL)
定位結構化日誌原生支援功能齊全的傳統日誌框架.NET 內建抽象層
設定方式Fluent API + JSONXML 為主 + JSONDI 容器設定
結構化支援原生 MessageTemplate後來支援,但非核心設計依賴底層 Provider
Sink / Target 生態200+ Sink80+ Target依賴第三方 Provider
學習曲線低(API 直覺)中(XML 設定較多)低(但功能有限)
推薦用法當底層實作遺留專案使用當介面層(搭配 Serilog 底層)
公司推薦方式

MEL ILogger<T> 當介面,Serilog 當實作。程式碼只依賴 Microsoft.Extensions.Logging.ILogger<T>,Serilog 負責底層的 Sink 輸出和 Enrichment。這樣既保持程式碼的框架獨立性,又能享受 Serilog 強大的結構化能力。


安裝

# 核心套件
dotnet add package Serilog

# 常用 Sink
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

# DI 整合(推薦)
dotnet add package Serilog.Extensions.Logging

# JSON 設定支援
dotnet add package Serilog.Settings.Configuration

# 常用 Enricher
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Environment

核心概念

Logger 與 LogEvent

Serilog 的運作圍繞幾個核心概念:

概念說明
Logger日誌記錄器,負責接收 log 呼叫並分派給 Sink
LogEvent一筆日誌事件,包含 Timestamp、Level、MessageTemplate、Properties、Exception
Sink日誌輸出目標(Console、File、DB、遠端伺服器等)
Enricher自動為每筆 LogEvent 附加額外屬性(ThreadId、MachineName 等)
MessageTemplate日誌訊息模板,{PropertyName} 語法擷取結構化屬性

日誌等級

由低到高,六個等級:

等級用途工業場景範例
Verbose最詳細的追蹤資訊每筆 Modbus frame 原始 byte
Debug開發階段除錯PLC Register 讀取值
Information正常運作的重要事件設備連線成功、Recipe 切換
Warning異常但系統仍可運作通訊重試、溫度接近上限
Error錯誤,功能受影響PLC 通訊失敗、Recipe 執行錯誤
Fatal系統無法繼續運作主控程式崩潰、資料庫無法連線

MessageTemplate 語法

MessageTemplate 是 Serilog 的靈魂。它看起來像字串插值,但運作方式完全不同:

// ⚠️ 字串插值($"..."):產生純文字,失去結構化能力
Log.Information($"PLC {plcId} register {address} = {value}"); // 不要這樣寫

// ✅ MessageTemplate:屬性被獨立擷取
Log.Information("PLC {PlcId} register {Address} = {Value}", plcId, address, value);

特殊運算子:

// @ 運算子:物件解構(Destructuring)— 展開物件的屬性
var status = new { Mode = "Auto", Temperature = 25.5, Pressure = 1.2 };
Log.Information("Device status: {@DeviceStatus}", status);
// 產生:{ "Mode": "Auto", "Temperature": 25.5, "Pressure": 1.2 }

// $ 運算子:強制 ToString()
Log.Information("Raw data: {$RawBytes}", byteArray);
// 產生:"System.Byte[]" 而非展開陣列

Log Pipeline

每次呼叫 Log.Information(...) 等 API 都會建立一個 LogEvent,依序通過 Enricher、Filter,最後由 Sink Routing 分派到一個或多個輸出目標:

各階段重點:

  • LogEvent 是不可變資料:一旦建立後 Enricher 只會「附加」屬性,不能修改既有屬性
  • Enricher 順序很重要:先註冊的 Enricher 先執行;自訂 Enricher 通常依賴前面的 Enricher 已附加的屬性
  • Filter 一次篩選MinimumLevel 是全域底線,子 Sink 可用 restrictedToMinimumLevel 各自加嚴
  • Sink 是廣播式:通過 Filter 的 LogEvent 會送到所有設定的 Sink,並非擇一—多 Sink 同時寫入是常態

Sink 路由:依屬性分流

進階情境會依 Level 或 Property 把 LogEvent 路由到不同 Sink,例如把警報級以上的事件額外送到 Seq,正常事件只進檔案:

對應的 LoggerConfiguration 寫法(簡化版):

Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.WithThreadId()
.Enrich.WithMachineName()
.WriteTo.Console()
.WriteTo.File("logs/equipment-.log", rollingInterval: RollingInterval.Day)
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(le => le.Level >= LogEventLevel.Warning)
.WriteTo.Seq("http://seq.local"))
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(le => le.Properties.ContainsKey("HasAuditTag"))
.WriteTo.File("logs/audit-.log", rollingInterval: RollingInterval.Day))
.CreateLogger();

子 Logger(WriteTo.Logger(lc => ...))是路由策略的關鍵:每個子 Logger 都有自己的 Filter,只有通過的 LogEvent 才會進入該分支的 Sink。


最小範例

using Serilog;

// 建立 Logger
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/equipment-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();

// 使用
Log.Information("Equipment control system starting");
Log.Debug("Connecting to PLC {PlcId} at {IpAddress}:{Port}", "PLC-01", "192.168.1.10", 502);

try
{
// 設備操作...
Log.Information("PLC {PlcId} connected successfully", "PLC-01");
}
catch (Exception ex)
{
Log.Error(ex, "Failed to connect PLC {PlcId}", "PLC-01");
}
finally
{
// 確保所有日誌都寫入完成
await Log.CloseAndFlushAsync();
}

延伸閱讀