Skip to main content

公司場景 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 vs Stopwatch

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 抽象。遷移策略:

  1. 短期ILoggerService 實作內部改用 Serilog 當底層,對外介面不變
  2. 中期:新模組直接使用 ILogger<T>(MEL 介面),舊模組逐步遷移
  3. 長期ILoggerService 標記為 [Obsolete],統一使用 ILogger<T>
注意

遷移過程中,同一個應用程式可能同時存在 ILoggerServiceILogger<T> 兩套日誌介面。確保兩者底層都指向同一個 Serilog Logger,避免日誌分散。


延伸閱讀