前言
一個系統中,Log 對於開發人員來說是非常重要的,所以在系統建置之初,我一定是先把 Log 放進去。
在 ASP.NET Core 統一透過 ILogger 來記錄 Log,至於要使用那一個 Logging Provider 則可以讓我們自由選擇。
Effective Logging in ASP.NET Core 課程提到,系統中,Log 應該保有以下幾項特性,
完整及一致的資訊
當錯誤發生時,應該要可以知道是發生在那一個 Page or Route ,參數是什麼,甚至登入者是誰。
所以,完整的資訊可以避免讓我們再詢問使用者,他做了什麼事,他是怎麼做的…保持程式碼簡潔
我們應該不會在每個地方全都加上 try…catch ,可以的話,應該使用 Global handlers, Filters 或是 Attributes ,以避免在程式碼中寫太多的 try…catch 。易於使用
Log 除了可讓開發人員透過文字檔查看外,應該可以提供其他的方式讓測試、開發或是營運人員查看。減少修正問題的困難度
當錯誤發時生時,可以依 Log 提供的資訊,清楚問題所在,輕易地修正錯誤。更了解系統
好的 Log 可以讓我們更了解我們的系統,例如記錄 API 的效能資訊,呼叫次數、平均、最長及最短時間,發現異常之處。提供處理優先順序
有了 Log 所提供的彙總資訊,可以開發人員決定那個問題或是那個部份的效能要優先處理。
練習實作
練習方案為 rainmakerho/RMStore ,請先切到 NativeConsole Branch(git checkout NativeConsole)。
使用預設的 Console Logging Provider
啟始專案為 RMStore.WebUI 及 RMStore.API ,透過 WebUI 登入後建立 Auth Cookie 及 Token (為了練習方便,User資訊及 JWT 設定在 appSettings.json 之中。
在呼叫 API 時,會透過 StandardHttpMessageHandler Class 從 HttpContext 取出 token 放到 Header Authorization 之中,然後取回 Products 資料。
所以過程如下,
因為預設的 Logger 為 Console Logging Provider ,所以我們在 Razor Page 中寫的 Log 會呈現在「偵錯」之中,如下,
使用 Serilog 寫到 File
實際使用上,會將 Log 寫到 File or DB 之中,所以可以透過 Serilog 先將 Log 寫到檔案之中。
安裝 Serilog 相關套件
所以在方案上按下右鍵選擇「管理方案的 Nuget 套件(N)…」
將套件「Serilog.Sinks.File」、「Serilog.Settings.Configuration」及 「Serilog.Extensions.Hosting」 加入到 RMStore.WebUI 及 RMStore.API 專案之中。
RMStore.WebUI 使用 Serilog
1 | public class Program |
Log的檔案可以在 appSettings.json 中設定,所以將原本 Logging 區段換成 Serilog ,如下,
1 | { |
RMStore.API 使用 Serilog
1 | public class Program |
Log的檔案一樣是在 appSettings.json 中設定,如下,
1 | { |
目前 RMStore.WebUI 及 RMStore.API 都是寫到同一個 Log 檔之中,所以在 WriteTo 參數是使用 shared 為 true
RMStore.Domain 加入 Log
在 ProductRepository Class 中的 GetAllProducts Method 加入 Log ,如下,
1 | public class ProductRepository : IProductRepository |
執行程式
執行程式後,Log 會寫到 products.json 之中,一開始是啟動 API, WebUI 的 Log ,接著是開啟登入頁,然後登入成功後,會建立 HttpClient 呼叫 API 專案取得 Products 的資料。
整個 Log 的內容蠻完整的,MessageTemplate 中的參數會對應到 Properties , SourceContext 為寫 Log 的所在 Class,RequestId 為那次 Http Request 的 id 。
1 | {"Timestamp":"2020-08-20T14:03:31.1146793+08:00","Level":"Information","MessageTemplate":"Start API Application"} |
最後方案為 rainmakerho/RMStore-serilog ,請先切到 serilog Branch(git checkout serilog)。
處理錯誤
已將 Log 寫到實體檔案之中了,所以當發生錯誤時,可以使用 Error Page 來呈現錯誤。
而 API 專案跟 WebUI 一樣,可以使用 Middleware 來 Handle Exception。
RMStore.API 使用 Middleware 來 Handle Error
在 RMStore.API 專案中,新增 Middleware 目錄,放存到 API Error 錯誤處理的相關檔案,
ApiError.cs
1 | public class ApiError |
ApiExceptionMiddleware.cs
1 | public class ApiExceptionMiddleware |
ApiExceptionMiddlewareExtensions.cs
1 | public static class ApiExceptionMiddlewareExtensions |
ApiExceptionOptions.cs
1 | public class ApiExceptionOptions |
所以在 RMStore.API 的 Startup.cs 就可以使用 UseApiExceptionHandler ,app.UseDeveloperExceptionPage 就不用了哦! 如下,
1 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
RMStore.Domain 專案中 ProductRepository 加入 引發 SqlException
可以在 RMStore.Domain 專案中 ProductRepository.cs 中的 GetAllProducts ,當 productName 為 error ,就會引發 SQLException
SqlExceptionCreator.cs
1 | //https://stackoverflow.com/questions/1386962/how-to-throw-a-sqlexception-when-needed-for-mocking-and-unit-testing |
ProductRepository.cs : 當輸入 error 就會有 throw 一個 SqlException
1 | public class ProductRepository : IProductRepository |
RMStore.WebUI 專案加入錯誤處理
RMStore.WebUI 的 Startup.cs 直接使用 UseExceptionHandler ,app.UseDeveloperExceptionPage 就不用了哦! 如下,
1 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
StandardHttpMessageHandler 是處理呼叫 api 的地方,所以在 RMStore.API 那會傳遞 ApiError 物件過來,所以將這些內容寫到 Exception.Data 之中,如下,
1 | public class StandardHttpMessageHandler : DelegatingHandler |
Product.cshtml.cs 處理查詢產品資料,如果輸入值為 patherror ,就會給錯誤的 url 來摸擬 url 出錯的狀況。
1 | public class ProductModel : PageModel |
因為錯誤會導到 Error 頁面,所以可以取出 HttpContext.Features.Get
Error.cshtml
1 | @page |
Error.cshtml.cs
1 | [ ] |
- 註: Error.cshtml.cs 在 OnGet and OnPost 都有呼叫 ProcessError 是因為如果是在 OnGet 發生錯誤時,就會執行 Error 的 OnGet,如果是在 OnPost 發生錯誤,就會執行 Error 的 OnPost 。
執行程式
所以如果是 Get 發生錯誤,會被導到 Error Page,
如果是 Post 發生 api url 錯誤,也會放導到 Error Page,
如果是 Post 發生 api 中的 DB 錯誤,也會放導到 Error Page,
最後方案為 rainmakerho/RMStore-err ,請先切到 err Branch(git checkout err)。
參考資料
Fundamentals of Logging in .NET Core
High-performance logging with LoggerMessage in ASP.NET Core
How to include scopes when logging exceptions in ASP.NET Core
The semantics of ILogger.BeginScope()
Structured logging concepts in .NET Series (1)
Authentication And Authorization In ASP.NET Core Web API With JSON Web Tokens
Razor custom error page ignores it’s OnGet handler