公司場景 Pattern
Pattern 1: 設備通訊日誌
PLC、SECS/GEM 等通訊是設備控制的核心。通訊日誌需要記錄完整上下文才有診斷價值。
基本通訊記錄
public class ModbusCommunicationService
{
private readonly ILogger<ModbusCommunicationService> _logger;
public async Task<ushort[]> ReadRegistersAsync(string plcId, int address, int count)
{
_logger.LogDebug("PLC {PlcId} reading registers {Address}+{Count}",
plcId, address, count);
var stopwatch = Stopwatch.StartNew();
try
{
var result = await _modbusClient.ReadHoldingRegistersAsync(address, count);
_logger.LogDebug(
"PLC {PlcId} read {Address}+{Count} = [{Values}] in {ElapsedMs}ms",
plcId, address, count,
string.Join(", ", result),
stopwatch.ElapsedMilliseconds);
return result;
}
catch (TimeoutException ex)
{
_logger.LogWarning(ex,
"PLC {PlcId} communication timeout reading {Address}+{Count} after {ElapsedMs}ms",
plcId, address, count, stopwatch.ElapsedMilliseconds);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex,
"PLC {PlcId} communication error reading {Address}+{Count}",
plcId, address, count);
throw;
}
}
}
SECS/GEM 通訊記錄
public class SecsGemLogger
{
private readonly ILogger<SecsGemLogger> _logger;
public void LogMessageSent(int stream, int function, string description)
{
_logger.LogInformation(
"SECS sent S{Stream}F{Function} {Description}",
stream, function, description);
}
public void LogMessageReceived(int stream, int function, byte[] rawData)
{
_logger.LogDebug(
"SECS received S{Stream}F{Function} raw={@RawData} length={Length}",
stream, function, rawData, rawData.Length);
}
public void LogAlarmReport(int alarmId, string alarmText, bool isSet)
{
if (isSet)
{
_logger.LogWarning(
"SECS alarm SET: AlarmId={AlarmId} Text={AlarmText}",
alarmId, alarmText);
}
else
{
_logger.LogInformation(
"SECS alarm CLEAR: AlarmId={AlarmId} Text={AlarmText}",
alarmId, alarmText);
}
}
}
Pattern 2: 操作審計日誌(FDA Part 11 合規)
FDA Part 11 要求記錄所有操作者行為,包含誰、什麼時候、做了什麼、結果如何。搭配 Metalama 的 [Audit] Aspect 可以自動化這個流程。
手動審計記錄
public class RecipeService
{
private readonly ILogger<RecipeService> _logger;
public async Task<bool> ExecuteRecipeAsync(string recipeId, string operatorId)
{
using (LogContext.PushProperty("OperatorId", operatorId))
using (LogContext.PushProperty("RecipeId", recipeId))
using (LogContext.PushProperty("AuditCategory", "RecipeExecution"))
{
_logger.LogInformation(
"Recipe execution STARTED by operator {OperatorId}: {RecipeId}",
operatorId, recipeId);
try
{
var result = await RunRecipeStepsAsync(recipeId);
_logger.LogInformation(
"Recipe execution COMPLETED: {RecipeId} result={@Result}",
recipeId, result);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex,
"Recipe execution FAILED: {RecipeId}",
recipeId);
return false;
}
}
}
public async Task ModifyRecipeAsync(string recipeId, string operatorId, RecipeParams oldParams, RecipeParams newParams)
{
_logger.LogWarning(
"Recipe MODIFIED by {OperatorId}: {RecipeId} from {@OldParams} to {@NewParams}",
operatorId, recipeId, oldParams, newParams);
await SaveRecipeAsync(recipeId, newParams);
}
}
搭配 Metalama [Audit] Aspect
當公司的 Metalama Aspect 機制就位後,審計記錄可以透過 Attribute 自動生成,減少手寫 log 的遺漏風險:
// Metalama Aspect 會自動在方法前後生成審計日誌
[Audit("RecipeExecution")]
public async Task<bool> ExecuteRecipeAsync(string recipeId, string operatorId)
{
// 只需要專注於業務邏輯
return await RunRecipeStepsAsync(recipeId);
}
Pattern 3: 效能計量
使用 SerilogTimings 套件自動計算操作耗時,不需要手動管理 Stopwatch。
dotnet add package SerilogTimings
using SerilogTimings;
public class BatchProcessService
{
private readonly ILogger<BatchProcessService> _logger;
public async Task ProcessLotAsync(string lotId, IReadOnlyList<Wafer> wafers)
{
// 自動記錄耗時,完成時產生 Information 等級日誌
using (Operation.Time("Processing lot {LotId} with {WaferCount} wafers", lotId, wafers.Count))
{
foreach (var wafer in wafers)
{
using (Operation.Time("Inspecting wafer {WaferId}", wafer.Id))
{
await InspectWaferAsync(wafer);
}
// 自動記錄:Inspecting wafer W-001 completed in 1.2s
}
}
// 自動記錄:Processing lot LOT-001 with 25 wafers completed in 32.5s
}
public async Task<EquipmentStatus> PollDeviceStatusAsync(string deviceId)
{
// 超時時自動升級為 Warning
using (Operation.At(LogEventLevel.Debug)
.Time("Polling device {DeviceId} status", deviceId))
{
return await _deviceClient.GetStatusAsync(deviceId);
}
}
}
Operation.Time 的優勢不只是省幾行程式碼。它在作用域結束時自動記錄耗時,還能在操作失敗(拋出例外)時自動標記為 Abandoned,等級從 Information 升為 Warning。手寫 Stopwatch 要自己處理這些邊界情況。
Pattern 4: 例外處理與 LogContext
利用 LogContext 在錯誤傳播鏈中逐層加入上下文,讓最終的 Error log 包含完整診斷資訊:
public class EquipmentController
{
private readonly ILogger<EquipmentController> _logger;
public async Task RunProductionCycleAsync(ProductionOrder order)
{
using (LogContext.PushProperty("OrderId", order.Id))
using (LogContext.PushProperty("ProductType", order.ProductType))
{
_logger.LogInformation("Production cycle started for order {OrderId}", order.Id);
foreach (var step in order.Steps)
{
using (LogContext.PushProperty("StepName", step.Name))
using (LogContext.PushProperty("StepIndex", step.Index))
{
try
{
await ExecuteStepAsync(step);
}
catch (EquipmentException ex)
{
// 這條 Error 自動帶有 OrderId, ProductType, StepName, StepIndex
_logger.LogError(ex,
"Step {StepName} failed in order {OrderId}",
step.Name, order.Id);
throw;
}
}
}
}
}
}
輸出範例(所有上下文屬性自動附加):
2026-04-10 14:30:05.123 [ERR] Step "Dispense" failed in order ORD-001
OrderId: ORD-001
ProductType: Wafer-8inch
StepName: Dispense
StepIndex: 3
Exception: EquipmentException: Pump pressure below threshold...
Pattern 5: DI 整合(ILogger<T>)
這是公司推薦的標準方式:用 Microsoft.Extensions.Logging.ILogger<T> 當介面,Serilog 當底層實作。
dotnet add package Serilog.Extensions.Logging
在 Generic Host 應用程式中設定
// Program.cs 或 App.xaml.cs
var host = Host.CreateDefaultBuilder(args)
.UseSerilog((context, services, configuration) =>
{
configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day);
})
.ConfigureServices((context, services) =>
{
services.AddSingleton<IDeviceMonitorService, DeviceMonitorService>();
})
.Build();
在服務中使用 ILogger<T>
public class DeviceMonitorService : IDeviceMonitorService
{
private readonly ILogger<DeviceMonitorService> _logger;
private readonly IModbusClient _modbusClient;
public DeviceMonitorService(
ILogger<DeviceMonitorService> logger, // DI 自動注入
IModbusClient modbusClient)
{
_logger = logger;
_modbusClient = modbusClient;
}
public async Task MonitorAsync(CancellationToken ct)
{
// _logger 的 SourceContext 自動是 "DeviceMonitorService"
_logger.LogInformation("Device monitoring started");
while (!ct.IsCancellationRequested)
{
var temperature = await _modbusClient.ReadTemperatureAsync();
_logger.LogDebug("Current temperature: {Temperature}°C", temperature);
if (temperature > 80.0)
{
_logger.LogWarning("Temperature {Temperature}°C exceeds threshold", temperature);
}
}
}
}
程式碼中完全沒有 using Serilog;,只依賴 Microsoft.Extensions.Logging。如果未來要換掉 Serilog,只改啟動設定即可,所有服務程式碼不用動。
Pattern 6: 搭配 Polly 記錄重試 / 熔斷事件
搭配 Polly 韌性策略,把每次重試、熔斷狀態變化都記錄下來:
public static class ResiliencePolicies
{
public static ResiliencePipeline CreatePlcCommunicationPipeline(
ILogger logger)
{
return new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
OnRetry = args =>
{
logger.LogWarning(
"PLC communication retry #{AttemptNumber} after {Delay}ms: {ExceptionMessage}",
args.AttemptNumber,
args.RetryDelay.TotalMilliseconds,
args.Outcome.Exception?.Message);
return ValueTask.CompletedTask;
}
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
MinimumThroughput = 10,
SamplingDuration = TimeSpan.FromSeconds(30),
BreakDuration = TimeSpan.FromSeconds(60),
OnOpened = args =>
{
logger.LogError(
"Circuit breaker OPENED — PLC communication suspended for {BreakDuration}s",
args.BreakDuration.TotalSeconds);
return ValueTask.CompletedTask;
},
OnClosed = args =>
{
logger.LogInformation("Circuit breaker CLOSED — PLC communication resumed");
return ValueTask.CompletedTask;
}
})
.Build();
}
}
與現有 GST ILoggerService 的關係
GST Framework 目前有自己的 ILoggerService 抽象。遷移策略:
- 短期:
ILoggerService實作內部改用 Serilog 當底層,對外介面不變 - 中期:新模組直接使用
ILogger<T>(MEL 介面),舊模組逐步遷移 - 長期:
ILoggerService標記為[Obsolete],統一使用ILogger<T>
遷移過程中,同一個應用程式可能同時存在 ILoggerService 和 ILogger<T> 兩套日誌介面。確保兩者底層都指向同一個 Serilog Logger,避免日誌分散。
延伸閱讀
- 概觀 — 結構化日誌基礎、安裝
- Sink 與設定 — Sink 詳解、設定方式、Enricher
- Polly 韌性策略 — 重試、熔斷策略
- Metalama 技術指南 — Aspect 自動化審計日誌