### 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欄位有正確被設定並且寫入資訊

<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/)