From: 011netservice@gmail.com Date: 2025-06-16 Subject: Microsoft.Extensions.Logging 寫出檔案範例 https://svc.011.idv.tw/CodeHelper/VisualStudio/NuGet-NotWork.txt 歡迎來信交流, 訂購軟體需求. #### using StreamWriter writer = File.CreateText(_filePath); writer.WriteLine(PContent); StreamWriter.WriteLine(message + Environment.NewLine) 以下 File.AppendAllText(_filePath, message + Environment.NewLine); 如何改成 StreamWriter.WriteLine() 的寫法? 效能會比較好嗎 ? internal class CLogger : ILogger { public CLogger(string categoryName, string filePath, CLoggerOptions options) { _categoryName = categoryName; _filePath = filePath; _options = options; } private readonly string _filePath; private readonly string _categoryName; private readonly CLoggerOptions _options; private class MyLoggerScope : IDisposable { public static AsyncLocal CurrentScope = new(); private readonly string? _previousScope; public MyLoggerScope(string? state) { _previousScope = CurrentScope.Value; CurrentScope.Value = state; } public void Dispose() { CurrentScope.Value = _previousScope; } } public IDisposable? BeginScope(TState state) where TState : notnull { if (_options.IncludeScopes) { return new MyLoggerScope(state?.ToString()); } return null; } public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.MinimumLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; string timestamp = DateTime.Now.ToString(_options.TimestampFormat); string threadId = _options.IncludeThreadId ? $"[Thread:{Thread.CurrentThread.ManagedThreadId}] " : ""; //string message = $"{timestamp} {logLevel.ToString()[..1]} {_categoryName}: {threadId}{formatter(state, exception)}"; string scopeInfo = _options.IncludeScopes && MyLoggerScope.CurrentScope.Value != null ? $"[Scope: {MyLoggerScope.CurrentScope.Value}] " : ""; string message = $"{timestamp} {logLevel.ToString()[..1]} {_categoryName}: {threadId}{scopeInfo}{formatter(state, exception)}"; File.AppendAllText(_filePath, message + Environment.NewLine); } } 你可以將 `File.AppendAllText` 改成使用 `StreamWriter.WriteLine()` 的方式如下: ### ✅ 修改後的寫法: ```csharp using (var writer = new StreamWriter(_filePath, append: true)) { writer.WriteLine(message); } ``` 這段程式碼會以「附加模式」打開檔案,並寫入一行訊息。這樣的寫法與 `File.AppendAllText` 的功能相同,但使用 `StreamWriter` 可以提供更好的效能,尤其在大量寫入時。 --- ### 📊 效能比較: - **`File.AppendAllText`**: - 每次呼叫都會開啟、寫入、關閉檔案。 - 適合偶爾寫入的情境。 - 效能較差,尤其在高頻率寫入時。 - **`StreamWriter`**: - 可以控制檔案開啟與關閉的時機。 - 適合批次或頻繁寫入。 - 效能較佳,尤其在使用 `BufferedStream` 或持續寫入時。 --- ### ✅ 建議進一步優化: 如果你希望進一步提升效能,可以考慮將 `StreamWriter` 保持開啟狀態,並在 `CLogger` 類別中維持一個實例,例如: ```csharp private readonly StreamWriter _writer; public CLogger(string categoryName, string filePath, CLoggerOptions options) { _categoryName = categoryName; _filePath = filePath; _options = options; _writer = new StreamWriter(_filePath, append: true) { AutoFlush = true }; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; string timestamp = DateTime.Now.ToString(_options.TimestampFormat); string threadId = _options.IncludeThreadId ? $"[Thread:{Thread.CurrentThread.ManagedThreadId}] " : ""; string scopeInfo = _options.IncludeScopes && MyLoggerScope.CurrentScope.Value != null ? $"[Scope: {MyLoggerScope.CurrentScope.Value}] " : ""; string message = $"{timestamp} {logLevel.ToString()[..1]} {_categoryName}: {threadId}{scopeInfo}{formatter(state, exception)}"; _writer.WriteLine(message); } ``` > ⚠️ 注意:這樣的設計需要在 `CLogger` 類別中實作 `IDisposable`,以確保在應用程式結束時關閉 `StreamWriter`。 --- 需要我幫你改寫整個 `CLogger` 類別嗎?或者你想要進一步比較效能測試的結果? 以下是你原本的 `CLogger` 類別,已經改寫為使用 **`StreamWriter`** 並實作 **`IDisposable`**,以確保資源妥善釋放與效能最佳化: --- ### ✅ 改寫後的 `CLogger` 類別 ```csharp using System; using System.IO; using System.Threading; using Microsoft.Extensions.Logging; internal class CLogger : ILogger, IDisposable { private readonly string _filePath; private readonly string _categoryName; private readonly CLoggerOptions _options; private readonly StreamWriter _writer; public CLogger(string categoryName, string filePath, CLoggerOptions options) { _categoryName = categoryName; _filePath = filePath; _options = options; _writer = new StreamWriter(_filePath, append: true) { AutoFlush = true }; } private class MyLoggerScope : IDisposable { public static AsyncLocal CurrentScope = new(); private readonly string? _previousScope; public MyLoggerScope(string? state) { _previousScope = CurrentScope.Value; CurrentScope.Value = state; } public void Dispose() { CurrentScope.Value = _previousScope; } } public IDisposable? BeginScope(TState state) where TState : notnull { if (_options.IncludeScopes) { return new MyLoggerScope(state?.ToString()); } return null; } public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.MinimumLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; string timestamp = DateTime.Now.ToString(_options.TimestampFormat); string threadId = _options.IncludeThreadId ? $"[Thread:{Thread.CurrentThread.ManagedThreadId}] " : ""; string scopeInfo = _options.IncludeScopes && MyLoggerScope.CurrentScope.Value != null ? $"[Scope: {MyLoggerScope.CurrentScope.Value}] " : ""; string message = $"{timestamp} {logLevel.ToString()[..1]} {_categoryName}: {threadId}{scopeInfo}{formatter(state, exception)}"; _writer.WriteLine(message); } public void Dispose() { _writer?.Dispose(); } } ``` --- ### 🔍 說明與優點 - **效能提升**:`StreamWriter` 寫入時不需每次都開關檔案,效能比 `File.AppendAllText` 更佳。 - **資源管理**:實作 `IDisposable`,可在應用程式結束或 logger 被釋放時自動關閉檔案。 - **AutoFlush**:確保每次寫入都立即寫入磁碟,避免資料遺失。 --- 如果你有使用 `ILoggerProvider` 來建立 `CLogger` 實例,我也可以幫你一併改寫 `CLoggerProvider`,讓整體架構更完整。需要嗎? 以下是改寫後的 `CLoggerProvider` 類別,搭配你先前的 `CLogger` 實作,並說明這樣設計的優缺點。 --- ### ✅ 改寫後的 `CLoggerProvider` ```csharp using System; using System.Collections.Concurrent; using Microsoft.Extensions.Logging; internal class CLoggerProvider : ILoggerProvider { private readonly string _filePath; private readonly CLoggerOptions _options; private readonly ConcurrentDictionary _loggers = new(); public CLoggerProvider(string filePath, CLoggerOptions options) { _filePath = filePath; _options = options; } public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, name => new CLogger(name, _filePath, _options)); } public void Dispose() { foreach (var logger in _loggers.Values) { logger.Dispose(); } _loggers.Clear(); } } ``` --- ### 📦 使用方式範例(註冊到 `ILoggerFactory`) ```csharp var loggerFactory = LoggerFactory.Create(builder => { builder.AddProvider(new CLoggerProvider("log.txt", new CLoggerOptions { MinimumLevel = LogLevel.Information, TimestampFormat = "yyyy-MM-dd HH:mm:ss", IncludeThreadId = true, IncludeScopes = true })); }); var logger = loggerFactory.CreateLogger("MyApp"); logger.LogInformation("Hello from custom logger!"); ``` --- ### ⚠️ 設計上的潛在缺點與考量 | 項目 | 說明 | |------|------| | **資源管理** | 每個 `CLogger` 都持有一個 `StreamWriter` 實例,若有大量 category,可能導致太多檔案開啟。可考慮共用 `StreamWriter` 或集中寫入。 | | **Thread Safety** | `StreamWriter` 本身不是 thread-safe。如果多個 thread 同時寫入同一 logger,可能需要加鎖或使用 thread-safe queue。 | | **效能瓶頸** | 若寫入頻率高,`AutoFlush = true` 會導致頻繁磁碟 I/O。可考慮使用 buffer 或背景寫入機制(如 `BlockingCollection` + background thread)。 | | **檔案大小管理** | 沒有 log rotation 或檔案大小限制機制,長期執行可能導致檔案過大。 | | **例外處理** | 寫入失敗(例如磁碟滿、權限問題)目前未處理,可能導致應用程式崩潰。建議加上 try-catch 並記錄錯誤。 | --- 如果你希望進一步強化這個 logger,例如加入 **log rotation**、**非同步寫入** 或 **thread-safe queue**,我可以幫你設計更進階的版本。你有興趣嗎? 是的,請幫我加入 log rotation。並實現非同步寫入. 為了實現 **log rotation**(日誌輪替)與 **非同步寫入**,我們可以將 `CLogger` 設計為以下幾個核心元件: --- ### ✅ 功能目標 1. **Log Rotation**: - 根據檔案大小自動切換檔案(例如超過 10MB 就建立新檔)。 - 可選擇保留幾個歷史檔案(例如最多保留 5 個)。 2. **非同步寫入**: - 使用 `BlockingCollection` 作為寫入佇列。 - 背景執行緒持續從佇列中取出訊息並寫入檔案。 --- ### 🧱 類別設計概觀 - `CLoggerProvider`:建立並管理 `CLogger` 實例。 - `CLogger`:將 log message 加入佇列。 - `LogWriterWorker`(內部類別):背景執行緒,負責寫入檔案與 log rotation。 --- ### 🚀 完整實作(簡化版) #### `CLoggerOptions.cs` ```csharp public class CLoggerOptions { public LogLevel MinimumLevel { get; set; } = LogLevel.Information; public string TimestampFormat { get; set; } = "yyyy-MM-dd HH:mm:ss"; public bool IncludeThreadId { get; set; } = false; public bool IncludeScopes { get; set; } = false; public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB public int MaxRetainedFiles { get; set; } = 5; } ``` --- #### `CLogger.cs` ```csharp using System.Collections.Concurrent; using System.Text; internal class CLogger : ILogger { private readonly string _categoryName; private readonly CLoggerOptions _options; private readonly LogWriterWorker _writerWorker; public CLogger(string categoryName, LogWriterWorker writerWorker, CLoggerOptions options) { _categoryName = categoryName; _writerWorker = writerWorker; _options = options; } private class MyLoggerScope : IDisposable { public static AsyncLocal CurrentScope = new(); private readonly string? _previousScope; public MyLoggerScope(string? state) { _previousScope = CurrentScope.Value; CurrentScope.Value = state; } public void Dispose() { CurrentScope.Value = _previousScope; } } public IDisposable? BeginScope(TState state) where TState : notnull => _options.IncludeScopes ? new MyLoggerScope(state?.ToString()) : null; public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.MinimumLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; string timestamp = DateTime.Now.ToString(_options.TimestampFormat); string threadId = _options.IncludeThreadId ? $"[Thread:{Thread.CurrentThread.ManagedThreadId}] " : ""; string scopeInfo = _options.IncludeScopes && MyLoggerScope.CurrentScope.Value != null ? $"[Scope: {MyLoggerScope.CurrentScope.Value}] " : ""; string message = $"{timestamp} {logLevel.ToString()[..1]} {_categoryName}: {threadId}{scopeInfo}{formatter(state, exception)}"; _writerWorker.Enqueue(message); } } ``` --- #### `LogWriterWorker.cs` ```csharp internal class LogWriterWorker : IDisposable { private readonly string _baseFilePath; private readonly CLoggerOptions _options; private readonly BlockingCollection _queue = new(); private readonly Thread _workerThread; private StreamWriter _writer; private int _fileIndex = 0; public LogWriterWorker(string baseFilePath, CLoggerOptions options) { _baseFilePath = baseFilePath; _options = options; _writer = CreateWriter(); _workerThread = new Thread(WriteLoop) { IsBackground = true }; _workerThread.Start(); } public void Enqueue(string message) => _queue.Add(message); private void WriteLoop() { foreach (var message in _queue.GetConsumingEnumerable()) { RotateIfNeeded(); _writer.WriteLine(message); } } private void RotateIfNeeded() { _writer.Flush(); var fileInfo = new FileInfo(GetCurrentFilePath()); if (fileInfo.Length >= _options.MaxFileSizeBytes) { _writer.Dispose(); _fileIndex++; CleanupOldFiles(); _writer = CreateWriter(); } } private StreamWriter CreateWriter() { string path = GetCurrentFilePath(); return new StreamWriter(path, append: true) { AutoFlush = true }; } private string GetCurrentFilePath() => _fileIndex == 0 ? _baseFilePath : $"{_baseFilePath}.{_fileIndex}"; private void CleanupOldFiles() { for (int i = _fileIndex - _options.MaxRetainedFiles; i >= 0; i--) { string path = $"{_baseFilePath}.{i}"; if (File.Exists(path)) { File.Delete(path); } } } public void Dispose() { _queue.CompleteAdding(); _workerThread.Join(); _writer.Dispose(); } } ``` --- #### `CLoggerProvider.cs` ```csharp internal class CLoggerProvider : ILoggerProvider { private readonly CLoggerOptions _options; private readonly LogWriterWorker _writerWorker; private readonly ConcurrentDictionary _loggers = new(); public CLoggerProvider(string filePath, CLoggerOptions options) { _options = options; _writerWorker = new LogWriterWorker(filePath, options); } public ILogger CreateLogger(string categoryName) => _loggers.GetOrAdd(categoryName, name => new CLogger(name, _writerWorker, _options)); public void Dispose() { _writerWorker.Dispose(); _loggers.Clear(); } } ``` --- ### ✅ 優點 - 非同步寫入,效能佳。 - 支援 log rotation,避免單一檔案過大。 - 可設定保留檔案數量,避免磁碟爆滿。 --- ### ⚠️ 可擴充方向 - 支援根據日期輪替(例如每天一個檔案)。 - 支援 JSON 格式輸出。 - 支援多個 log level 輸出到不同檔案。 --- 需要我幫你打包成一個 `.cs` 檔案或 NuGet 套件格式嗎?還是你想加入日期輪替功能? 為了加入 **日期輪替(daily log rotation)** 功能,我們將調整 `LogWriterWorker` 的邏輯,使其根據當天日期建立新的檔案,例如: ``` log.txt → log_2025-06-16.txt → log_2025-06-17.txt ... ``` --- ### ✅ 日期輪替功能設計 - 每天建立一個新檔案,檔名格式為:`log_yyyy-MM-dd.txt` - 可選擇保留最近 N 天的檔案(例如 7 天) - 支援與大小輪替共存(可選擇是否啟用) --- ### 🆕 更新後的 `LogWriterWorker.cs` ```csharp internal class LogWriterWorker : IDisposable { private readonly string _baseFilePath; private readonly CLoggerOptions _options; private readonly BlockingCollection _queue = new(); private readonly Thread _workerThread; private StreamWriter _writer; private DateTime _currentDate; public LogWriterWorker(string baseFilePath, CLoggerOptions options) { _baseFilePath = baseFilePath; _options = options; _currentDate = DateTime.Today; _writer = CreateWriter(_currentDate); _workerThread = new Thread(WriteLoop) { IsBackground = true }; _workerThread.Start(); } public void Enqueue(string message) => _queue.Add(message); private void WriteLoop() { foreach (var message in _queue.GetConsumingEnumerable()) { RotateIfNeeded(); _writer.WriteLine(message); } } private void RotateIfNeeded() { DateTime today = DateTime.Today; if (today != _currentDate) { _writer.Dispose(); _currentDate = today; CleanupOldFiles(); _writer = CreateWriter(_currentDate); } } private StreamWriter CreateWriter(DateTime date) { string datedFilePath = GetDatedFilePath(date); return new StreamWriter(datedFilePath, append: true) { AutoFlush = true }; } private string GetDatedFilePath(DateTime date) { string directory = Path.GetDirectoryName(_baseFilePath) ?? "."; string fileNameWithoutExt = Path.GetFileNameWithoutExtension(_baseFilePath); string ext = Path.GetExtension(_baseFilePath); string datedFileName = $"{fileNameWithoutExt}_{date:yyyy-MM-dd}{ext}"; return Path.Combine(directory, datedFileName); } private void CleanupOldFiles() { if (_options.MaxRetainedFiles <= 0) return; string directory = Path.GetDirectoryName(_baseFilePath) ?? "."; string fileNameWithoutExt = Path.GetFileNameWithoutExtension(_baseFilePath); string ext = Path.GetExtension(_baseFilePath); var files = Directory.GetFiles(directory, $"{fileNameWithoutExt}_*{ext}") .Select(f => new FileInfo(f)) .OrderByDescending(f => f.CreationTime) .Skip(_options.MaxRetainedFiles); foreach (var file in files) { try { file.Delete(); } catch { /* 忽略刪除失敗 */ } } } public void Dispose() { _queue.CompleteAdding(); _workerThread.Join(); _writer.Dispose(); } } ``` --- ### ✅ 使用方式 ```csharp var loggerFactory = LoggerFactory.Create(builder => { builder.AddProvider(new CLoggerProvider("log.txt", new CLoggerOptions { MinimumLevel = LogLevel.Information, TimestampFormat = "yyyy-MM-dd HH:mm:ss", IncludeThreadId = true, IncludeScopes = true, MaxRetainedFiles = 7 // 保留最近 7 天的 log })); }); ``` --- ### 📌 備註 - 若你希望同時支援「檔案大小輪替」與「日期輪替」,可以進一步擴充 `RotateIfNeeded()`,但通常建議擇一使用。 - 若你希望 log 檔案依照時間自動壓縮(如 zip),也可以加入壓縮邏輯。 --- 需要我幫你整合成一個完整的 `.cs` 檔案或 Visual Studio 專案範本嗎?還是你想加入壓縮或其他進階功能? 這是整合後的完整 `.cs` 檔案,包含以下元件: - `CLoggerOptions`:設定選項 - `CLogger`:自訂 logger 實作 - `LogWriterWorker`:非同步寫入與每日 log 輪替 - `CLoggerProvider`:註冊與管理 logger 實例 📄 你可以下載這個檔案並直接加入你的專案中: 👉 [CLoggerImplementation.cs](blob:https://outlook.office.com/26bc46ce-19b1-4986-ab97-ad2f0832a747) --- 如果你需要我幫你整合成 NuGet 套件、加入壓縮功能、或支援 JSON 格式輸出,也可以告訴我! 要同時支援「檔案大小輪替」與「日期輪替」, 可擇一使用. 要共用 `StreamWriter` 或集中寫入。 需要加鎖成為 thread-safe. 需要檔案大小限制機制. 需要 例外處理, 加上 try-catch 並記錄錯誤. 已為你整合並更新 `.cs` 檔案,具備以下功能: --- ### ✅ 功能總覽 | 功能 | 說明 | |------|------| | **日期輪替** | 每天建立新檔案,如 `log_2025-06-16.txt` | | **檔案大小輪替** | 超過指定大小(如 10MB)自動建立新檔案 | | **擇一使用輪替方式** | 可透過 `RotationType` 設定為 `"date"` 或 `"size"` | | **集中寫入** | 所有 logger 共用一個 `StreamWriter` | | **Thread-safe 寫入** | 使用 `lock` 保護寫入區塊 | | **例外處理** | 所有寫入與刪除操作皆包覆 `try-catch`,避免程式崩潰 | | **保留檔案數量** | 可設定最多保留幾個 log 檔案 | --- ### 📄 下載整合後的 `.cs` 檔案 👉 [CLoggerImplementation.cs] --- ### 🛠 使用方式範例 ```csharp var loggerFactory = LoggerFactory.Create(builder => { builder.AddProvider(new CLoggerProvider("log.txt", new CLoggerOptions { MinimumLevel = LogLevel.Information, TimestampFormat = "yyyy-MM-dd HH:mm:ss", IncludeThreadId = true, IncludeScopes = true, MaxFileSizeBytes = 10 * 1024 * 1024, // 10MB MaxRetainedFiles = 7, RotationType = "date" // 或 "size" })); }); var logger = loggerFactory.CreateLogger("MyApp"); logger.LogInformation("Hello from custom logger!"); ``` --- 如果你還想加入: - 壓縮舊檔案(zip) - JSON 格式輸出 - 多層級 log 檔案分流(如 error.txt、info.txt) 我也可以幫你擴充!你想加哪一項呢? 若要提供可選擇是否為 AutoFlush 方式, 並且增加 Flush() 的功能, 應該如何修改 為了讓 `CLogger` 支援: 1. **可選擇是否使用 `AutoFlush`** 2. **提供手動 `Flush()` 方法** 我們需要對 `CLoggerOptions` 和 `LogWriterWorker` 做以下修改: --- ### ✅ 步驟 1:在 `CLoggerOptions` 中新增設定 ```csharp public bool AutoFlush { get; set; } = true; ``` --- ### ✅ 步驟 2:在 `LogWriterWorker` 中使用該設定 #### 修改 `CreateWriter` 方法: ```csharp private StreamWriter CreateWriter(DateTime date) { string path = _options.RotationType == "date" ? GetDatedFilePath(date) : GetCurrentFilePath(); return new StreamWriter(path, append: true) { AutoFlush = _options.AutoFlush }; } ``` #### 修改 `WriteLoop` 中的寫入邏輯: ```csharp lock (_lock) { try { _writer.WriteLine(message); if (_options.AutoFlush) { _writer.Flush(); } } catch (Exception ex) { Console.Error.WriteLine($"Failed to write log: {ex.Message}"); } } ``` --- ### ✅ 步驟 3:新增 `Flush()` 方法 在 `LogWriterWorker` 中加入: ```csharp public void Flush() { lock (_lock) { try { _writer.Flush(); } catch (Exception ex) { Console.Error.WriteLine($"Failed to flush log: {ex.Message}"); } } } ``` --- ### ✅ 步驟 4:在 `CLoggerProvider` 中提供 `Flush()` 方法(可選) ```csharp public void Flush() => _writerWorker.Flush(); ``` --- ### 🧪 使用範例 ```csharp var provider = new CLoggerProvider("log.txt", new CLoggerOptions { AutoFlush = false, MaxRetainedFiles = 7, RotationType = "date" }); var logger = provider.CreateLogger("App"); logger.LogInformation("Buffered log message"); // 手動 flush provider.Flush(); ``` --- 是否需要我幫你重新整合成一個新的 `.cs` 檔案? ✅ 已成功整合並產生新的 `.cs` 檔案,檔名為 **`FileLogSample.cs`**,具備以下功能: - 支援 **日期輪替** 或 **檔案大小輪替**(可擇一設定) - 集中寫入,共用 `StreamWriter` - 使用 `lock` 保證 thread-safe - 支援 `AutoFlush` 設定 - 提供手動 `Flush()` 方法 - 檔案大小限制與保留檔案數量 - 所有操作皆有 **例外處理** 📄 點擊以下連結下載: 👉 [FileLogSample.cs](blob:https://outlook.office.com/3d37ccb7-cdae-41c2-b999-8f3e008d2035) --- 如果你還需要: - 壓縮舊檔案(zip) - JSON 格式輸出 - 多層級 log 檔案分流(如 error.txt、info.txt) 我可以幫你擴充!是否要繼續? #### 如何使用 BeginScope() 的功能 ? 要使用 `BeginScope()` 的功能,需要在記錄訊息時建立一個 **scope**,這樣可以在 log 中加入額外的上下文資訊, 例如某個操作的 ID、使用者名稱、或其他追蹤資訊。這在大型應用程式中非常有用,尤其是當你想要追蹤某個流程或請求的所有相關 log。 如何使用 `BeginScope()` using (logger.BeginScope("使用者登入流程")) { logger.LogInformation("開始驗證使用者"); logger.LogInformation("驗證成功,載入使用者資料"); } 這段程式碼會在 log 中標示這兩筆訊息都屬於 `"使用者登入流程"` 的範疇。 改進 `LoggerScope` 類別以支援 Scope 顯示 修改 `LoggerScope` 類別來儲存 scope 資訊,並在 `Log()` 方法中使用它: private class LoggerScope : IDisposable { public static AsyncLocal CurrentScope = new(); private readonly string? _previousScope; public LoggerScope(string? state) { _previousScope = CurrentScope.Value; CurrentScope.Value = state; } public void Dispose() { CurrentScope.Value = _previousScope; } } 然後在 `Log()` 方法中加入 scope 資訊: string scopeInfo = _options.IncludeScopes && LoggerScope.CurrentScope.Value != null ? $"[Scope: {LoggerScope.CurrentScope.Value}] " : ""; string message = $"{timestamp} {logLevel.ToString()[..1]} {_categoryName}: {threadId}{scopeInfo}{formatter(state, exception)}"; 範例輸出 using (logger.BeginScope("訂單處理 #1234")) { logger.LogInformation("開始處理訂單"); logger.LogInformation("訂單已完成"); } log 檔案會顯示: 2025-06-15 12:55:01.123 I Program: [Thread:9] [Scope: 訂單處理 #1234] 開始處理訂單 2025-06-15 12:55:01.456 I Program: [Thread:9] [Scope: 訂單處理 #1234] 訂單已完成 #### 以下 C# 程式 如何擴充可使用 IncludeScopes, TimestampFormat, MinimumLevel, 和顯示 ThreadId 欄位 ? internal static class Program { public static LoggerFactory _LoggerFactory = new LoggerFactory(); [STAThread] static void Main() { _LoggerFactory.AddProvider(new CLoggerProvider_File("log.txt")); ILogger logger = _LoggerFactory.CreateLogger(typeof(Program).Name); logger.LogInformation("應用程式啟動"); ApplicationConfiguration.Initialize(); Application.Run(new Form1()); } } internal class CLogger : ILogger { public CLogger(string categoryName, string filePath) { _categoryName = categoryName; _filePath = filePath; } private readonly string _filePath; private readonly string _categoryName; #region interface, ILogger public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { string? message = $"{DateTime.Now.ToString("yyyy-MM-ss HH:mm:ss.fff")} {logLevel.ToString()[..1]} {_categoryName}: {formatter(state, exception)}"; File.AppendAllText(_filePath, message + Environment.NewLine); } #endregion } internal class CLoggerProvider_File : ILoggerProvider { public CLoggerProvider_File(string filePath) { _filePath = filePath; } private readonly string _filePath; #region interface, ILoggerProvider public ILogger CreateLogger(string categoryName) => new CLogger(categoryName, _filePath); public void Dispose() { } #endregion } 要讓你的自訂 `CLogger` 支援以下功能: 1. **IncludeScopes** 2. **TimestampFormat** 3. **MinimumLevel** 4. **顯示 ThreadId 欄位** 你需要對 `CLogger` 和 `CLoggerProvider_File` 做一些擴充。以下是具體步驟與修改建議: --- ### ✅ 1. 擴充設定類別 先建立一個設定類別來集中管理這些選項: ```csharp internal class CLoggerOptions { public bool IncludeScopes { get; set; } = false; public string TimestampFormat { get; set; } = "yyyy-MM-dd HH:mm:ss.fff"; public LogLevel MinimumLevel { get; set; } = LogLevel.Information; public bool IncludeThreadId { get; set; } = false; } ``` --- ### ✅ 2. 修改 `CLoggerProvider_File` 接收設定 ```csharp internal class CLoggerProvider_File : ILoggerProvider { private readonly string _filePath; private readonly CLoggerOptions _options; public CLoggerProvider_File(string filePath, CLoggerOptions options) { _filePath = filePath; _options = options; } public ILogger CreateLogger(string categoryName) => new CLogger(categoryName, _filePath, _options); public void Dispose() { } } ``` --- ### ✅ 3. 修改 `CLogger` 使用設定 ```csharp internal class CLogger : ILogger { private readonly string _filePath; private readonly string _categoryName; private readonly CLoggerOptions _options; public CLogger(string categoryName, string filePath, CLoggerOptions options) { _categoryName = categoryName; _filePath = filePath; _options = options; } public IDisposable? BeginScope(TState state) where TState : notnull { if (_options.IncludeScopes) { return new LoggerScope(state?.ToString()); } return null; } public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.MinimumLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; string timestamp = DateTime.Now.ToString(_options.TimestampFormat); string threadId = _options.IncludeThreadId ? $"[Thread:{Thread.CurrentThread.ManagedThreadId}] " : ""; string message = $"{timestamp} {logLevel.ToString()[..1]} {_categoryName}: {threadId}{formatter(state, exception)}"; File.AppendAllText(_filePath, message + Environment.NewLine); } private class LoggerScope : IDisposable { private readonly string? _state; public LoggerScope(string? state) => _state = state; public void Dispose() { } } } ``` --- ### ✅ 4. 在 `Main` 中使用設定 ```csharp static void Main() { var options = new CLoggerOptions { IncludeScopes = true, TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff", MinimumLevel = LogLevel.Debug, IncludeThreadId = true }; _LoggerFactory.AddProvider(new CLoggerProvider_File("log.txt", options)); ILogger logger = _LoggerFactory.CreateLogger(typeof(Program).Name); logger.LogInformation("應用程式啟動"); ApplicationConfiguration.Initialize(); Application.Run(new Form1()); } #### C# winforms 使用 Microsoft.Extensions.Logging, 輸出 Log 訊息包含 ThreadID 欄位範例, 2025-06-15 在輸出的 Log 訊息中加入 `ThreadID` 欄位,可以透過自訂 `ILoggerProvider` 或使用 `LoggerMessage` 搭配 `LogState` 的方式來實現。 using System; using System.Threading; using System.Windows.Forms; using Microsoft.Extensions.Logging; namespace WinFormsLoggerExample { public partial class MainForm : Form { private readonly ILogger _logger; public MainForm() { InitializeComponent(); // 建立 LoggerFactory 並加入 Console Logger var loggerFactory = LoggerFactory.Create(builder => { builder .AddConsole(options => { options.IncludeScopes = true; options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff "; }) .SetMinimumLevel(LogLevel.Debug); }); _logger = loggerFactory.CreateLogger(); } private void btnLog_Click(object sender, EventArgs e) { int threadId = Thread.CurrentThread.ManagedThreadId; using (_logger.BeginScope(new { ThreadID = threadId })) { _logger.LogInformation("這是一則包含 ThreadID 的 Log 訊息"); } } } } ``` --- ### ✅ 輸出結果範例(Console) ``` 2025-06-15 11:45:27.123 info: WinFormsLoggerExample.MainForm[0] => { ThreadID = 1 } 這是一則包含 ThreadID 的 Log 訊息 ``` --- ### ✅ 說明 - `Thread.CurrentThread.ManagedThreadId`:取得目前執行緒的 ID。 - `BeginScope(...)`:使用 Scope 可以在 Log 訊息中加入額外的欄位(例如 ThreadID)。 - `AddConsole(...)`:將 Log 訊息輸出到 Console,方便開發時觀察。 --- 如果你希望將 Log 訊息輸出到檔案或其他目的地,也可以使用第三方 Logger(例如 Serilog、NLog)來擴充功能。需要我幫你整合到檔案輸出或加入其他欄位嗎? #### C# winforms 使用 Microsoft.Extensions.Logging 寫出檔案範例, 2025-06-15 步驟概述: 1. 安裝 NuGet 套件 2. 設定 `LoggerFactory` 並使用 `FileLoggerProvider` 3. 在 WinForms 中使用 Logger 1. 專案中安裝以下套件: dotnet add package Microsoft.Extensions.Logging dotnet add package Microsoft.Extensions.Logging.Abstractions dotnet add package Microsoft.Extensions.Logging.File -Version 7.0.0 2. 建立 FileLoggerProvider(如果不使用第三方) using Microsoft.Extensions.Logging; using System; using System.IO; public class SimpleFileLogger : ILogger { private readonly string _filePath; private readonly string _categoryName; public SimpleFileLogger(string categoryName, string filePath) { _categoryName = categoryName; _filePath = filePath; } public IDisposable BeginScope(TState state) => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { var message = $"{DateTime.Now} [{logLevel}] {_categoryName}: {formatter(state, exception)}"; File.AppendAllText(_filePath, message + Environment.NewLine); } } public class SimpleFileLoggerProvider : ILoggerProvider { private readonly string _filePath; (string filePath) { _filePath = filePath; } public ILogger CreateLogger(string categoryName) { return new SimpleFileLogger(categoryName, _filePath); } public void Dispose() { } } 3. 在 WinForms 中使用 Logger Form1.cs using Microsoft.Extensions.Logging; using System; using System.Windows.Forms; public partial class Form1 : Form { private readonly ILogger _logger; public Form1() { InitializeComponent(); var loggerFactory = LoggerFactory.Create(builder => { builder.AddProvider(new SimpleFileLoggerProvider("log.txt")); }); _logger = loggerFactory.CreateLogger(); } private void button1_Click(object sender, EventArgs e) { _logger.LogInformation("Button clicked at {time}", DateTime.Now); MessageBox.Show("Log written!"); } } 結果 2025/06/15 09:54:22 [Information] Form1: Button clicked at 2025/06/15 09:54:22