メインコンテンツまでスキップ

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} 產生三字元大寫等級(INFWRNERR),{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?

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 整合