Sink 與設定
什麼是 Sink
Sink 是 Serilog 的日誌輸出目標。每個 Sink 負責把 LogEvent 寫入一個特定的地方。你可以同時使用多個 Sink — 例如開發時同時輸出到 Console 和 File,生產環境再加上資料庫。
Serilog 的 Sink 生態非常豐富(200+),以下是公司場景最常用的幾個。
常用 Sink
Console — 開發除錯
最基本的 Sink,將日誌輸出到終端。開發階段必備。
dotnet add package Serilog.Sinks.Console
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
.CreateLogger();
{Level:u3} 產生三字元大寫等級(INF、WRN、ERR),{Message:lj} 讓字串維持原樣、物件用 JSON 格式。
File — 滾動檔案日誌
生產環境最基本的 Sink,支援依日期或檔案大小自動分割。
dotnet add package Serilog.Sinks.File
Log.Logger = new LoggerConfiguration()
.WriteTo.File("logs/equipment-.log",
rollingInterval: RollingInterval.Day, // 每日一個新檔案
rollOnFileSizeLimit: true, // 超過大小限制時也分割
fileSizeLimitBytes: 50_000_000, // 50 MB
retainedFileCountLimit: 90, // 保留 90 天
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
關鍵參數:
| 參數 | 說明 | 建議值 |
|---|---|---|
rollingInterval | 分割週期 | Day(一般)/ Hour(高流量) |
fileSizeLimitBytes | 單檔大小上限 | 50MB(避免開檔過慢) |
retainedFileCountLimit | 保留檔案數 | 90(三個月) |
rollOnFileSizeLimit | 超過大小也分割 | true |
shared | 多 Process 共用 | true(同一設備多服務時) |
Seq — 結構化日誌搜尋伺服器
Seq 是專門為結構化日誌設計的搜尋與分析伺服器。所有 Serilog 的結構化屬性都能直接在 Web UI 上查詢和視覺化。
dotnet add package Serilog.Sinks.Seq
Log.Logger = new LoggerConfiguration()
.WriteTo.Seq("http://seq-server:5341")
.CreateLogger();
Seq 適合多台設備、多個服務需要集中查看日誌的場景。單一設備的 WPF 應用程式用 File Sink 就夠了。當設備數量超過 5 台,或需要跨設備關聯分析時,Seq 的價值才會明顯。Seq 單人開發免費,團隊使用需要商業授權。
SQLite / PostgreSQL — 寫入資料庫
適合需要長期保存、結構化查詢的場景。搭配 TimescaleDB(PostgreSQL 擴充)可以做時序分析。
# SQLite(輕量,適合單機設備)
dotnet add package Serilog.Sinks.SQLite
# PostgreSQL(適合搭配 TimescaleDB)
dotnet add package Serilog.Sinks.PostgreSQL
// SQLite — 設備端本地日誌資料庫
Log.Logger = new LoggerConfiguration()
.WriteTo.SQLite("logs/equipment.db",
tableName: "DeviceLogs",
batchSize: 100)
.CreateLogger();
Debug — Visual Studio Output Window
開發時在 VS Output Window 看日誌,方便下中斷點對照。
dotnet add package Serilog.Sinks.Debug
Log.Logger = new LoggerConfiguration()
.WriteTo.Debug() // 輸出到 System.Diagnostics.Debug
.CreateLogger();
設定方式
Serilog 支援兩種設定方式,可以混合使用。
Fluent API(程式碼設定)
直接在 C# 中用 Builder Pattern 設定,好處是有 IntelliSense 輔助,適合不需要外部調整的設定:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.Enrich.WithMachineName()
.WriteTo.Console()
.WriteTo.File("logs/equipment-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
appsettings.json(外部設定)
透過 Serilog.Settings.Configuration 套件,從 JSON 設定檔讀取組態。好處是不用改程式碼就能調整日誌行為(例如臨時開 Debug 等級排查問題):
dotnet add package Serilog.Settings.Configuration
{
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning",
"MyCompany.Equipment.Communication": "Debug"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "logs/equipment-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 90,
"fileSizeLimitBytes": 50000000
}
}
],
"Enrich": ["FromLogContext", "WithThreadId", "WithMachineName"]
}
}
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
混用的最佳實踐
推薦做法是:基礎設定用 JSON(可外部調整),程式碼只做 JSON 做不到的事:
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration) // JSON 負責 Sink 和等級
.Enrich.WithProperty("Application", "EquipmentControl") // 程式碼加固定屬性
.CreateLogger();
多 Sink 同時輸出
可以對不同 Sink 設定不同的最小等級,實現日誌分流:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
// Console:只顯示 Information 以上(開發時不被 Debug 洗版)
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
// File:記錄所有 Debug 以上(完整追蹤)
.WriteTo.File("logs/equipment-.log",
rollingInterval: RollingInterval.Day)
// 另一個 File:只記錄 Error 以上(快速定位問題)
.WriteTo.File("logs/errors-.log",
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: LogEventLevel.Error)
.CreateLogger();
MinimumLevel 與 Override
MinimumLevel 設定全域最低等級,Override 可以針對特定 Namespace 調整:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
// Microsoft 和 System 的 log 太多,只要 Warning 以上
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
// 公司設備通訊模組需要 Debug(排查通訊問題)
.MinimumLevel.Override("GatherTech.Equipment.Communication", LogEventLevel.Debug)
.CreateLogger();
Override 的 Namespace 比對是前綴匹配:GatherTech.Equipment.Communication 會套用到所有以此開頭的 Logger,包括 GatherTech.Equipment.Communication.Modbus。
Enricher
Enricher 自動為每筆 LogEvent 附加額外屬性,不需要在每次 log 呼叫時手動帶入。
內建 Enricher
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Process
Log.Logger = new LoggerConfiguration()
.Enrich.WithThreadId() // 線程 ID
.Enrich.WithMachineName() // 機器名稱
.Enrich.WithProcessId() // Process ID
.Enrich.FromLogContext() // 動態上下文屬性(重要!)
.CreateLogger();
自訂 Enricher — 設備 ID、工站 ID
公司場景常需要每筆日誌都帶上設備識別資訊:
public class EquipmentEnricher : ILogEventEnricher
{
private readonly string _equipmentId;
private readonly string _stationId;
public EquipmentEnricher(string equipmentId, string stationId)
{
_equipmentId = equipmentId;
_stationId = stationId;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddPropertyIfAbsent(
propertyFactory.CreateProperty("EquipmentId", _equipmentId));
logEvent.AddPropertyIfAbsent(
propertyFactory.CreateProperty("StationId", _stationId));
}
}
// 使用
Log.Logger = new LoggerConfiguration()
.Enrich.With(new EquipmentEnricher("CMP-01", "BAY-3"))
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss}] [{Level:u3}] [{EquipmentId}/{StationId}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
// 輸出:[14:30:05] [INF] [CMP-01/BAY-3] Device connected successfully
LogContext — 動態上下文屬性
LogContext 讓你在一個作用域內臨時加入屬性,該作用域內所有 log 都會自動帶上:
// 確保啟用 FromLogContext
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.CreateLogger();
// 在處理某個 Lot 時,推入 LotId
using (LogContext.PushProperty("LotId", "LOT-2026-001"))
using (LogContext.PushProperty("RecipeId", "RECIPE-STD-CLEAN"))
{
Log.Information("Starting recipe execution"); // 自動帶 LotId + RecipeId
Log.Information("Step 1: Dispense chemical"); // 自動帶 LotId + RecipeId
Log.Warning("Chemical level low: {Level}%", 15); // 自動帶 LotId + RecipeId
}
// 離開 using 後,LotId 和 RecipeId 自動移除
公司場景:標準日誌設定
WPF 應用程式標準設定
public static class SerilogBootstrap
{
public static void Configure(IConfiguration configuration)
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.Enrich.WithMachineName()
.Enrich.WithProperty("Application", "EquipmentControl")
// Console:開發用
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
// 主日誌:每日分割
.WriteTo.File("logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 90,
fileSizeLimitBytes: 50_000_000,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
// 錯誤日誌:獨立檔案快速定位
.WriteTo.File("logs/errors-.log",
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: LogEventLevel.Error,
retainedFileCountLimit: 180)
.CreateLogger();
}
}
設備控制程式的日誌分類
針對不同類型的日誌寫入不同檔案,方便分開追查:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
// 通訊日誌(PLC、SECS/GEM)
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(Matching.FromSource("GatherTech.Communication"))
.WriteTo.File("logs/communication-.log", rollingInterval: RollingInterval.Day))
// 操作日誌(使用者操作、Recipe 執行)
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(Matching.FromSource("GatherTech.Operation"))
.WriteTo.File("logs/operation-.log", rollingInterval: RollingInterval.Day))
// 警報日誌
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(Matching.FromSource("GatherTech.Alarm"))
.WriteTo.File("logs/alarm-.log", rollingInterval: RollingInterval.Day))
// 全量日誌(所有來源)
.WriteTo.File("logs/all-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
延伸閱讀
- 概觀 — 結構化日誌基礎、MessageTemplate、日誌等級
- 公司場景 Pattern — 設備通訊、審計、效能計量、DI 整合