### serilog 的 日誌等級 | **Level** | Usage | | :--: | :-- | | ==Verbose== |Verbose is the noisiest level, rarely (if ever) enabled for a production app. | ==Debug== | Debug is used for internal system events that are not necessarily observable from the outside, but useful when determining how something happened. | ==Information== | Information events describe things happening in the system that correspond to its responsibilities and functions. Generally these are the observable actions the system can perform. | ==Warning== | When service is degraded, endangered, or may be behaving outside of its expected parameters, Warning level events are used. | ==Error== |When functionality is unavailable or expectations broken, an Error event is used. | ==Fatal== | The most critical level, Fatal events demand immediate attention. ### 安裝套件 這裡用 ==WEB API== 當作範例。 要在 Asp.Net Core 中加入日誌的機制,需要安裝以下套件。 [Serilog.AspNetCore](https://www.nuget.org/packages/Serilog.AspNetCore/) 這個套件已經整合了許多共用的相依套件,只要安裝這個就有日誌的基本功能(Console,file) ```shell dotnet add package Serilog.AspNetCore ``` ### 基本設定 **第一步**, 移除 ==appsetting.json== 中的 =="Logging"== 區段,並加入 =="Serilog"== 區段。 ```json= "Serilog": { "MinimumLevel": { "Default": "Information", //預設Log等級 "Override": { "Microsoft.AspNetCore": "Warning" // AspNetCore 預設等級。 } }, "Enrich": [ "FromLogContext" ], //輸出 Log 的位置,可以多個輸出設定 "WriteTo": [ // 輸出 Log 到 Console { "Name": "Console" }, // 輸出 Log 到 檔案 { "Name": "File", "Args": { "rollingInterval": "Day", //以「天」為單位存放檔案 "path": "./logs/log-.txt" //產生的檔名會是 log-yyyymmdd.txt } } ] } ``` **第二步**,在 Program.cs 主程式中加入以下程式碼,設定從 appsetting.json 讀取 Log 設定 ```csharp= var builder = WebApplication.CreateBuilder(args); Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); ``` **第三步**,在 Program.cs 主程式中加入 ==UseSerilog==,告訴應用程式使用Serilog作為日誌記錄器。 ```csharp= builder.Host.UseSerilog(); //<--加入這一行 var app = builder.Build(); ``` **第四步**,加入==app.UseSerilogRequestLogging==Middleware 來整理所有與 Request 相關的紀錄,讓你在一條 Log 中就可以取得目前 Request 所有的相關資訊。 而且可以另外設定一些自訂的內容:例如自訂Log的範本,或是額外新增其他資訊到Log。 ```csharp= //這個指令告訴應用程式使用中介軟體來記錄HTTP請求的詳細信息。 app.UseSerilogRequestLogging(options => { // 如果要自訂訊息的範本格式,可以修改這裡,但修改後並不會影響結構化記錄的屬性 options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; // 預設輸出的紀錄等級為 Information,你可以在此修改記錄等級 //options.GetLevel = (httpContext, elapsed, ex) => LogEventLevel.Information; // 你可以從 httpContext 取得 HttpContext 下所有可以取得的資訊! options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { //diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); //diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme); diagnosticContext.Set("UserID", httpContext.User.Identity?.Name); // 使用者 ID }; }); ``` **第五步**,完整的 program.cs 的內容如下 ```csharp= try { var builder = WebApplication.CreateBuilder(args); //setting logger and read from appsettings.json Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); Log.Information("This is a information log message"); Log.Warning("This is a warning log message"); Log.Error("This is an error log message"); Log.Fatal("This is a fatal log message"); // Add services to the container. builder.Services.AddControllers(); //這個指令告訴應用程式使用Serilog作為日誌記錄器。 builder.Host.UseSerilog(); var app = builder.Build(); app.UseSerilogRequestLogging(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } catch (Exception ex) { Log.Error(ex, "Application start-up failed"); } finally { Log.CloseAndFlush(); } ``` 由於 Log 的欄位很多,改用 Serilog.Formatting.Compact 來記錄 JSON 格式的 Log 訊息會清楚很多! 以下是 ==WriteTo== 的內容 ```json { "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft.AspNetCore": "Warning" } }, "Enrich": [ "FromLogContext" ], "WriteTo": [ { "Name": "Console" }, { "Name": "File", "Args": { "path": "./logs/log-.json", //以「天」為單位存放檔案 "rollingInterval": "Day", // setting the formatter "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" } } ] }, "AllowedHosts": "*" } ``` 以下是發出 ==GET /WeatherForecast== API 呼叫的 Log 內容 ```json { "@t": "2024-05-09T07:07:18.9931142Z", "@mt": "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms", "@r": [ "61.4462" ], "@tr": "1a152b9fa32c2bee51b0036b5af214a8", "@sp": "f9455e1ea28e4ec8", "RequestMethod": "GET", "RequestPath": "/weatherforecast", "StatusCode": 200, "Elapsed": 61.4462, "SourceContext": "Serilog.AspNetCore.RequestLoggingMiddleware", "RequestId": "0HN3FUQL2EOUE:00000001", "ConnectionId": "0HN3FUQL2EOUE", "Application": "SerilogDemo" } ``` ### 將 Log 存到 SQL Server 將日誌記錄在 SQL Server ,需要安裝這個套件 [Serilog.Sinks.MSSqlServer](https://www.nuget.org/packages/Serilog.Sinks.MSSqlServer) ```shell dotnet add package Serilog.Sinks.MSSqlServer ``` 雖然有提供自動建立表格的設定,但我的習慣是先建立好 資料庫 / 表格 。 ```sql= CREATE TABLE [Logs] ( [Id] int IDENTITY(1,1) NOT NULL, [Message] nvarchar(max) NULL, [MessageTemplate] nvarchar(max) NULL, [Level] nvarchar(128) NULL, [TimeStamp] datetime NOT NULL, [Exception] nvarchar(max) NULL, [Properties] nvarchar(max) NULL CONSTRAINT [PK_Logs] PRIMARY KEY CLUSTERED ([Id] ASC) ); ``` 新增使用者以及對應的權限。 ```sql= CREATE ROLE [SerilogWriter]; GRANT SELECT TO [SerilogWriter]; GRANT INSERT TO [SerilogWriter]; CREATE LOGIN [Serilog] WITH PASSWORD = 'password'; CREATE USER [Serilog] FOR LOGIN [Serilog] WITH DEFAULT_SCHEMA = dbo; GRANT CONNECT TO [Serilog]; ALTER ROLE [SerilogWriter] ADD MEMBER [Serilog]; ``` appsetting.json 中 的 WriteTo區塊如下 ```json= "WriteTo": [ { "Name": "Console" }, { "Name": "File", "Args": { "path": "./logs/log-.json", //以「天」為單位存放檔案 "rollingInterval": "Day", // setting the formatter "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" } }, { "Name": "MSSqlServer", //need to install package Serilog.Sinks.MSSqlServer "Args": { "connectionString": "your connectionString", "tableName": "Logs" } } ] ``` <br> ### 不同等級的日誌分開記錄到不同的檔案 有一個情境如下:想要將不同等級的日誌記錄到不同的檔案中,例如: ==Information== 等級記錄在 Info.json, ==Warning==以上等級記錄在 Error.json。 <br> 需要利用**表達式**來過濾日誌,因此安裝這個套件 [Serilog.Sinks.Expressions](https://www.nuget.org/packages/Serilog.Expressions) , ```shell dotnet add package Serilog.Expressions ``` appsetting.json 中 的 WriteTo區塊如下 ```json= "WriteTo": [ { "Name": "Console" }, { "Name": "Logger", "Args": { "configureLogger": { "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "(@l = 'Information' or @l = 'Debug')" } } ], "WriteTo": [ { "Name": "File", "Args": { "path": "./logs/Information/log-.json", "rollingInterval": "Day", "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" // setting the formatter } } ] } } }, { "Name": "Logger", "Args": { "configureLogger": { "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "(@l = 'Error' or @l = 'Fatal' or @l = 'Warning')" } } ], "WriteTo": [ { "Name": "File", "Args": { "path": "./logs/Warning/log-.json", "rollingInterval": "Day", "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" // setting the formatter } } ] } } } ] ``` <br> ### 將日誌存放到不同的 Table 另一種情境是:不希望日誌全部都在同一張表裡面,也許透過 Log 等級區分 或是 不同功能區分。 appsetting.json 中 的 WriteTo區塊如下 ```json= "WriteTo": [ { "Name": "Console" }, { "Name": "Logger", "Args": { "configureLogger": { "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "LogCategory = 'Application'" } } ], "WriteTo": [ { "Name": "MSSqlServer", "Args": { "connectionString": "your connectionString", "tableName": "ApplicationLogs" } } ] } } }, { "Name": "Logger", "Args": { "configureLogger": { "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "LogCategory = 'Business'" } } ], "WriteTo": [ { "Name": "MSSqlServer", "Args": { "connectionString": "your connectionString", "tableName": "BusinessLogs" } } ] } } } ] ``` 上面的 ==expression== 設定是透過 ==LogCategory== 屬性的值來決定要存放到哪一個表格 <br> 下面的 4 - 12 行是日誌紀錄的方法 ```csharp= [HttpGet] public IEnumerable<WeatherForecast> Get() { Log.ForContext("LogCategory", "Application").Information("這是一條應用層級的日誌1"); using (LogContext.PushProperty("LogCategory", "Application")) { _logger.LogInformation("這是一條應用層級的日誌2"); } Log.ForContext("LogCategory", "Business").Information("這是一條業務層級的日誌1"); using (LogContext.PushProperty("LogCategory", "Business")) { _logger.LogInformation("這是一條業務層級的日誌2"); } return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } ``` <br> ### 新增Log自訂欄位 在 SQL Server 的表格新增了一個欄位 [IP] varchar(50), 並且修改 ==appsettings.json== 的內容,新增7-15行的設定, ```json= "WriteTo": [ { "Name": "MSSqlServer", "Args": { "connectionString": "your connectionString", "tableName": "your tableName", "columnOptionsSection": { "additionalColumns": [ { "ColumnName": "IP", "DataType": "varchar", "DataLength": 50 } ] } } } ] ``` 然後寫入 log 的方式如下: 這裡的範例為:將這筆log 新增了 ==Logcategory== 欄位,設定為 ==Business==,代表這筆log 經過篩選後會寫到 ==BusinessLogs==表格,同時也新增了==IP==欄位。 ```csharp using (LogContext.PushProperty("LogCategory", "Business")) using (LogContext.PushProperty("IP", clientIP)) { _logger.LogInformation(Message); } ``` 程式執行後可以在 ==BusinessLog==表格查詢到以下資訊,IP欄位有正確被設定並且寫入資訊 ![image](https://hackmd.io/_uploads/H1mZCkym0.png) <br> ### 把Log相關功能獨立成服務 寫到這裡會發現,其實應該要把 Log 相關功能獨立出來成為一個 class / service,然後在 program.cs 中注入,如此一來,所有的controller就可以使用這個 LogSevice 進行寫入日誌的功能,以下是一個簡單的範例 <br> 新增一個 Class,名稱為 ==LogService.cs==,內容如下 ```csharp= // LogService.cs public class LogService { private readonly ILogger<LogService> _logger; public LogService(ILogger<LogService> logger) { _logger = logger; } public void LogInformationIP(string clientIP, string Message) { using (LogContext.PushProperty("LogCategory", "Business")) using (LogContext.PushProperty("IP", clientIP)) { _logger.LogInformation(Message); } } } ``` <br> 在 program.cs 注入==LogService==,位置在 13 行。 ```csharp= //program.cs try { var builder = WebApplication.CreateBuilder(args); //setting logger and read from appsettings.json Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); builder.Services.AddControllers(); builder.Services.AddScoped<LogService>(); <--新增這行 builder.Host.UseSerilog(); var app = builder.Build(); app.UseSerilogRequestLogging(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } catch (Exception ex) { Log.Error(ex, "Application start-up failed"); } finally { Log.CloseAndFlush(); } ``` <br> Controller 修改 _logger 的型別為 ==LogService==。 ```csharp= //controller private readonly LogService _logger; public WeatherForecastController(LogService logger) { _logger = logger; } [HttpGet] public IEnumerable<WeatherForecast> Get() { // add ip address to log string? ClientIP = HttpContext.Connection.RemoteIpAddress?.ToString(); ClientIP = ClientIP?.Replace("::1", "127.0.0.1"); _logger.LogInformationIP(ClientIP!, "這是一條帶有IP地址的日誌"); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } ``` <br> 這裡只是一個簡單的範例,實際上必須根據你的需求來設計 LogService 的內容。 --- 參考來源 [1.NET 6.0 如何使用 Serilog 對應用程式事件進行結構化紀錄-From Will保哥](https://blog.miniasp.com/post/2021/11/29/How-to-use-Serilog-with-NET-6) [2.Database Logging with Serilog in an ASP.NET Core Application](https://blog.fabritglobal.com/database-logging-serilog-asp-net-core/)