ASP.NET Core MVC 如何使用 Serilog 記錄日誌(1. 基於配置檔案的基本配置)
原文地址:
參考資料
- Serilog 官方文件:https://github.com/serilog/serilog/wiki
- serilog-aspnetcore 官方文件:https://github.com/serilog/serilog-aspnetcore#serilogaspnetcore
- Serilog.Settings.Configuration 官方文件:https://github.com/serilog/serilog-settings-configuration
- .netcore入門21:aspnetcore整合Serilog:https://blog.csdn.net/u010476739/article/details/105485315
0. 照例吐槽
Serilog 這個框架的中文資料相對來說比較少,中文資料還是 Log4net 和 NLog 的相對比較多。但我還是打算嘗試一下 Serilog 這個庫。在網上看過一些中文的資料,發現這些資料的配置方式都跟官方文件裡預設的配置方式相似,差不多是這樣:
我就算沒用過這個庫,看過 demo 後也明白個七七八八了。看上面那段程式碼中的.WriteTo.Console()
,估計意思就是日誌寫入到控制檯中,那如果需要把日誌寫入到檔案中,估計會是這樣的一個方法:.WriteTo.File(xxx,xxx,xxx...)
這樣配置顯然不行。這樣配置的話,如果我想切換一下日誌輸出的路徑,豈不是還要改程式碼重新發布?即使我日誌路徑從配置檔案中來取,那如果我想修改日誌記錄的等級,修改輸出日誌的渠道,我豈不是還要修改程式碼,重新發布專案?這種方式顯然只能用於自己的個人小 demo 專案。
我今天就是想實現一下完全從配置檔案來讀取對 Serilog 的日誌配置,這樣會更加靈活,在生產環境中用起來會更舒服。
1. 準備工作
首先,我先寫一個介面,用來測試log:
[HttpGet] public IActionResult LogTest() { try { throw new Exception("This is an exception"); } catch (Exception ex) { _logger.LogTrace(ex, "出現異常!"); _logger.LogDebug(ex, "出現異常!"); _logger.LogInformation(ex, "出現異常!"); _logger.LogWarning(ex, "出現異常!"); _logger.LogError(ex, "出現異常!"); _logger.LogCritical(ex, "出現異常!"); return Ok("Exception!"); } }
呼叫一下介面,看一下輸出的日誌:
可以看到 information 級別以下的日誌都沒有輸出,需要在 appsettings.json中配置一下:
後面日誌就會全輸出了。
2. 基本配置
引入 Serilog.AspNetCore 這個包,我這裡選擇穩定版本 4.1.0 。看一下這個包依賴了哪些包:
這裡截圖裡顯示的是 .NETCoreApp 3.1 的環境下依賴的包,.NET 5 環境下也是一樣的,不一樣的只是包的版本號。.NET 5 的在下面,我截圖截不到了。
僅僅看依賴的這些包名,我們可以看到該有的基本都有了。前三個自然不必說,Serilog.Extensions.Hosting 這個包顯然是用來在 Build ASP.NET 5 的 Host 的時候用的,我估計文件裡的.UseSerilog()
這個方法就在這個包裡。Serilog.Formatting.Compact 這個包顯然是用來格式化輸出的,估計很多使用者誇讚的把日誌用 JSON 格式來“結構化”輸出相關功能的程式碼就在這個包裡。Serilog.Settings.Configuration 這個包顯然是用來配置 Serilog 的。後面三個顯然是將日誌輸出到控制檯,Debug 視窗和檔案的三個包。
在 ASP.NET Core 的專案中配置 Serilog,自然要看 Serilog.AspNetCore 的文件,地址:https://github.com/serilog/serilog-aspnetcore
我們順著文件來。包安裝過程就不詳述了,在對應的目錄執行dotnet add xxxx
命令即可,文件裡已經寫了。
安裝完之後,開啟 ASP.NET Core 專案的 Program.cs
檔案,來進行 Serilog 的配置。我們看一眼官方文件:
上面黑灰色炭筆圈出來的部分我們不用管,實際用的時候也可以按照它寫的這個方式來寫,看上去挺靠譜的。但紅圈圈出來的這部分,是 new 一個LoggerConfiguration
物件,並進行一些配置,最後 Create 一個 Logger,並賦值給一個全域性的變數 Log 的 Logger 屬性。我估計配置好這一步之後,我們使用 ILogger 的實現的例項,也就是我前面寫的程式碼中的private readonly ILogger<HomeController> _logger;
來記錄日誌時,使用的就是這裡的配置。這個就與我們完全依賴配置檔案進行配置的想法相悖了,我也暫時覺得沒什麼必要來 Log 這個 Web 專案的啟動和退出過程。我們暫時不寫這一段程式碼。
然後最下方劃紅線的這個 UseSerilog 是必須的,我們要在程式碼裡寫上。
此時將專案 Run 起來,發現控制檯裡的日誌是這樣的:
正在生成,然後就沒下文了。然後我們用 Postman 呼叫一下介面:
可以拿到返回值,但控制檯還是什麼日誌都沒有輸出,可以說明程式碼已經開始跑了,記錄日誌的程式碼也都已經跑過了,只是沒有日誌輸出出來,說明我們的專案已經喪失了記錄日誌的能力。而且我這個專案是一個 MVC 專案,執行的時候會自動在瀏覽器開啟http://localhost:5000/
,顯示頁面,這次也沒有,我手動在瀏覽器裡訪問http://localhost:5000/
也無法訪問頁面,在瀏覽器例訪問http://localhost:5000/Home/LogTest
來嘗試呼叫介面,也會500。只有 Postman 能呼叫成功。估計是 ASP.NET Core MVC 日誌沒有配置好,導致整個專案啟動出現了問題,完全無法用瀏覽器來訪問。
根據我長達一年的程式設計經驗(雀實只有一年,很抱歉。幸運的是我得到了一些熱心的大佬的無私指導,不然估計現在還在 hello world),我猜測是因為我們使用了.UseSerilog()
來用 Serilog 接管了內建的日誌功能,但這個方法沒有配置好預設的配置程式,我們也沒有手動進行日誌的配置,所以出了這種問題。將.UseSerilog()
註釋掉則一切恢復正常。
3. 更進一步
為了尋求解決辦法,我們繼續閱讀文件:
這一段對我們有用的是化橙色線的幾句話。上面這一句是文件讓我們刪除掉配置檔案中的跟日誌相關的配置,改用 Serilog 的日誌配置。然後給了一個 Serilog configuration
的連結(https://github.com/serilog/serilog-settings-configuration)和一個示例專案the Sample project
的連結(https://github.com/serilog/serilog-aspnetcore/tree/dev/samples/Sample)。配置的連結指向的文件我們遲早要去看,估計這個文件是更詳細的配置項。我們這裡先看一下這個示例專案。我們在連結的 github 後面加個 '1s'(與那位年長的先生無關),然後將連結貼上到瀏覽器中,就可以直接用線上版的 VSCode 開啟這個示例:https://github1s.com/serilog/serilog-aspnetcore/tree/dev/samples/Sample
首先看一眼配置檔案:
可以看到預設的日誌相關配置檔案已經被刪掉,替換成了 Serilog 的。窺一斑而知全豹,我們先推測一下配置檔案的作用。上面那一段配置檔案MinimumLevel
起到的作用應該跟預設的配置檔案中的LogLevel
作用相似,是確定記錄的日誌最低等級的相關配置。
而下面這一端WriteTo
就非常有意思了,這顯然是配置了一個日誌輸出的渠道。其中Name
是File
,也就是說這段配置能夠把配置輸出到檔案中。那我們就基本可以確定,假設我們再補一個WriteTo
,把Name
配置為Console
,就能把日誌輸出到控制檯。
看到這裡,我們可以猜想 Serilog 是能夠只通過配置檔案來進行日誌配置的。
再看一下這個示例的Program.cs
檔案:
看來可以直接在這個方法裡面用委託來配置。但看到紅圈裡最後一行還是有一條.WriteTo.Console()
,我開始方了,難道一定要在這裡寫一個WriteTo
才能指定日誌輸出的渠道嗎?
我們把滑鼠移到UseSerilog()
上看一下:
怎麼感覺彈出來的方法簽名不太一樣,上面彈出的程式碼中,這個方法的宣告有一個 Action 委託引數,但這個委託只有兩個引數,一個 xxxContext, 一個 xxxConfiguration,然而這個檔案裡卻給了三個引數,中間多了個 Services。
但是我們管不了那麼多了,直接 ctrl + 滑鼠左鍵點進去看原始碼:
看原始碼就跟看英文閱讀理解一樣,先忽略掉意義不大的地方,比如說上面的判斷 null 的部分,專注英文文章中有實際意義的詞,也就是名詞、動詞、形容詞、副詞。在程式碼裡,也就是實際的程式碼邏輯部分。(其實我英語非常弱,六級都沒過,在這裡斗膽用英語閱讀理解做比喻真的是抱歉)。
上面原始碼中,使用了 IWebHostBuilder 的 ConfigureServices 方法,跟 Startup.cs 中的同名方法作用是差不多的。在這方法裡,傳入了一個委託,引數是 context 和 collection。在委託體中,new 了一個 loggerConfiguration 和 loggerProviders。其中 loggerConfiguration 會被我們在 Program.cs 中呼叫 UseSerilog() 方法時傳入的委託賦值,也就是它:
loggerProviders 的賦值,則取決於呼叫 UseSerilog() 時傳入的後兩個 bool 型別的值 preserveStaticLogger 和 writeToProviders,預設全是 false,這裡就全當 false 來看。因為 writeToProviders 是 false,loggerProviders 將不會被再次賦值,它的值會停留在null。
後面又用已經得到值的 loggerConfiguration 來 Create 出了一個 logger,接著建立了一個 registeredLogger,初始化為 null。由於 preserveStaticLogger 為 false,所以 registeredLogger 的值也停留在了 null,而全域性的 Log 變數的 Logger 屬性將會被賦值為前面剛剛被 Create 出來的 logger。
再後面就是用單例模式建立服務等操作,這部分程式碼不用看也能猜到是幹啥了。
這一段閱讀程式碼的部分,用文字敘述不清楚,如果是錄成視訊就更好了。在看我的部落格的朋友,如果跟我一樣基礎比較薄弱,可以認真看幾遍上面的原始碼,慢慢理解,不要著急。
看完這段程式碼,我們也基本知道了為什麼剛才程式跑不起來:
- 我們沒有呼叫 UseSerilog 方法時傳給它 configureLogger 這個 Action 委託,導致 UseSerilog 使用了另一個可以不傳任何引數的過載:
從而導致配置出錯,專案無法正常執行起來。
- 我們也沒有跟教程中說的那樣直接在 Main 方法中給 Log.Logger 賦值,就像這樣:
4. 嘗試配置
既然知道了原因,我們就傳一個委託進去。我們先把配置檔案改成跟上面示例中一樣:
但紅圈圈出來的值我們改成 Minute,原先的值是 Day,我們無法測試 Serilog 把日誌按時間切分的功能。改成分鐘應該能夠測試到。同時我給 WriteTo 這一節多加了兩段配置,期望它能將日誌輸出到控制檯和 Debug 視窗。
然後我們傳給 UseSerilog() 方法一個委託:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog((hostBuilderContext, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostBuilderContext.Configuration)
.Enrich.FromLogContext())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
其中loggerConfiguration.ReadFrom.Configuration(hostBuilderContext.Configuration)
是從專案設定的配置檔案,也就是預設的 appsettings.{environment}.json 中讀取配置。hostBuilderContext.Configuration
就是取專案配置好的的配置檔案。
然後.Enrich.FromLogContext()
的含義可以從這篇文件(https://github.com/serilog/serilog/wiki/Enrichment#the-logcontext)裡找到。大致翻譯一下就是“Serilog.Context.LogContext 可用於從環境“執行上下文”中動態新增和刪除屬性;例如,在事務期間寫入的所有訊息都可能帶有該事務的 id,等等。必須在配置時使用 .FromLogContext() 將此功能新增到記錄器。”,看起來是我沒有接觸過的高階功能。我連事務是什麼都不太清楚。先照抄。
5. 測試配置
將專案跑起來,重新用 Postman 呼叫介面:
可以看到日誌已經可以正常輸出到控制檯上了,非常好。再看一下這個應用程式的根目錄下有沒有產生我們在配置檔案中配置的 logs 資料夾,以及日誌有沒有被記錄到對應的日誌檔案中:
可以看到資料夾產生了,日誌也按照分鐘來分了,今天凌晨1點17分和1點29分分別都有日誌生成,被分配到了兩個檔案,檔案的字尾是202108220117和202108220129,非常好。
6. 總結,但並未完結
目前,日誌功能是基本上能用了,我們也實現了完全從配置檔案中讀取配置資訊來配置 Serilog,但還有幾點不夠完善,不夠規範:
- 日誌記錄到專案根路徑中不夠規範。如果這樣記的話,假設專案是用 Docker 釋出的,會不會把日誌記錄到 Docker 容器中去呢?(我也不太清楚)下次釋出,容器被替換,日誌可能就沒了。所以最好是記錄到伺服器的一個絕對路徑中。這樣也方便什麼 ELK 之類的(我也暫時沒用過,只是聽說過可以這樣用,算是個雲使用者吧,真是抱歉)從伺服器指定位置取日誌。
- 所有等級的日誌全都記錄到了一個檔案中,日誌並沒有根據日誌等級,即 Info,Error,Warning,Debug 等分到不同檔案中。我認為日誌分到不同檔案中才更方便查閱日誌來定位問題。
- 日誌不是結構化的,如果日誌被以 Json 格式,結構化地一條一條記錄下來,將會相當容易查詢。
我期望後面能夠實現上面的 2,3 條。我感覺這些只需要通過修改配置檔案來實現。應該需要查閱這份文件:https://github.com/serilog/serilog-settings-configuration
感謝閱讀,如果這篇部落格有幫到您的話,請點個贊,謝謝。