.NET 6 應用程式如何設定 Serilog

Serilog 已經是我們團隊的標準的 Log Provider,在此我針對它的配置粗略的紀錄,希望可以幫助到需要的人

開發環境

  • Windows 11
  • Rider

ASP.NET Core 配置 Serilog

開啟一個新的 ASP.NET Core 專案

dotnet new webapi -o Lab.SerilogProject.WebApi 

安裝 Serilog

進入目錄,安裝 Serilog 套件:

dotnet add package Serilog.AspNetCore --version 6.0.1

 

可以看到相關依賴的套件都準備好了

設定 Host 載體的 Serilog 組態

  • 初始化
Log.Logger = new LoggerConfiguration()
   .MinimumLevel.Information()
   .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
   .Enrich.FromLogContext()
   .WriteTo.Console()
   .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Hour)
   .CreateLogger();

 

  • 把 hosting 整段用 try/catch 包起來。
  • 讓 Host 使用 Serilog:builder.Host.UseSerilog()。
  • 每一個 Request 使用 Serilog 記錄下來:app.UseSerilogRequestLogging()。
  • 讓 Log 寫入 Sinks:Log.CloseAndFlush()。
  • finally 區段的確保應用程式當掉的時候能寫入serilog sinks
try
{
   Log.Information("Starting web host");
   var builder = WebApplication.CreateBuilder(args);
   builder.Host.UseSerilog();//<=== 讓 Host 使用 Serilog 
   
   // Add services to the container.
   builder.Services.AddControllers();
   // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
   builder.Services.AddEndpointsApiExplorer();
   builder.Services.AddSwaggerGen();
   var app = builder.Build();
   // Configure the HTTP request pipeline.
   if (app.Environment.IsDevelopment())
   {
       app.UseSwagger();
       app.UseSwaggerUI();
   }
   app.UseHttpsRedirection();
   app.UseSerilogRequestLogging(); //<=== 每一個 Request 使用 Serilog 記錄下來 
   app.UseAuthorization();
   app.MapControllers();
   app.Run();
   return 0;
}
catch (Exception ex)
{
   Log.Fatal(ex, "Host terminated unexpectedly");
   return 1;
}
finally
{
   Log.CloseAndFlush();
}

 

二階段初始化

第一次的初始化 Serilog 是為了要攔截 host 啟動/執行的異常,這時候還不能使用 host 的 DI Container,這時候 Configuration Provider 尚未工作,也就是 appsetting.json 還不能用,所幸 Serilog 支援二階段初始化,可以在 DI Container 套用 appsetting.json,步驟如下

  1. CreateLogger() 取代成 CreateBootstrapLogger()
  2. 使用 UseSerilog()
  3. ReadFrom.Configuration(context.Configuration):我們可以在 appsetting.json 設定有關 Serilog 的配置,通過該方法設定載入Configuration(appsetting.json),參考
  4. ReadFrom.Services(services):注入 enrichers and sinks,在每一個 pipeline 將會註冊以下,
    IDestructuringPolicy
    ILogEventEnricher
    ILogEventFilter
    ILogEventSink
    LoggingLevelSwitch

如下圖所示:

參考

serilog/serilog-aspnetcore: Serilog integration for ASP.NET Core (github.com)

除了二階段,我們也可以先行初始化 ConfigurationBuilder,然後再餵給 host 的 DI Container 以及 Serilog.Log

var config = new ConfigurationBuilder()
             .SetBasePath(Directory.GetCurrentDirectory())
             .AddJsonFile("appsettings.json")
             .Build;

 

餵給 Serilog.Log

Log.Logger = new LoggerConfiguration()
   .ReadFrom.Configuration(config)
   .....
   .CreateLogger();

 

有關 Configuration 可以參考

如何使用組態 Microsoft.Extensions.Configuration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

設定應用程式的 Serilog 組態

  • 指定應用程式的 Serilog 設定:builder.Host.UseSerilog
  • 從 host 讀取組態檔(例如:appsettings.json):config.ReadFrom.Configuration
builder.Host.UseSerilog((context, services, config) =>
    config.ReadFrom.Configuration(context.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .WriteTo.File("logs/aspnet-.txt", rollingInterval: RollingInterval.Minute)
    );

 

在 appsettings.json 檔案中的 Logging 區段移除並加入Serilog的區段,如下

"Serilog": {
 "MinimumLevel": {
   "Default": "Information",
   "Override": {
     "Microsoft": "Warning",
     "System": "Warning"h
   }
 }
}

 

更多的 Setting

serilog/serilog-settings-configuration: A Serilog configuration provider that reads from Microsoft.Extensions.Configuration (github.com)

 

在 Controller 裡面加入以下代碼

this._logger.LogInformation(new EventId(2000, "Trace"), "Start {ControllerName}.{MethodName}...",
    nameof(WeatherForecastController), nameof(Get));

 

執行結果如下:

 

.NET Console 配置 Serilog

接下來輪到 Console 應用程式了,新增一個 Console 專案

dotnet new console -o Lab.SerilogProject.ConsoleApp --framework net6.0

 

安裝套件

dotnet add package Microsoft.Extensions.Hosting --version 6.0.1
dotnet add package Serilog.Extensions.Hosting --version 5.0.1
dotnet add package Serilog.Formatting.Compact --version 1.1.0
dotnet add package Serilog.Settings.Configuration --version 3.3.0
dotnet add package Serilog.Sinks.Console --version 4.0.1
dotnet add package Serilog.Sinks.File --version 5.0.0
dotnet add package Serilog.Sinks.Seq --version 5.1.1

 

Console 的配置跟 ASP.NET Core 大同小異,就不再多述

Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Information()
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Day)
        .CreateBootstrapLogger()
    ;

try
{
    Log.Information("Starting host");

    var builder = Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<LabBackgroundService>();
        })
        .UseSerilog((context, services, config) =>
        {
            var formatter = new JsonFormatter();

            config.ReadFrom.Configuration(context.Configuration)
                .ReadFrom.Services(services)
                .Enrich.FromLogContext()
                .WriteTo.Console(formatter)
                .WriteTo.Seq("http://localhost:5341")
                .WriteTo.File(formatter, "logs/app-.txt", rollingInterval: RollingInterval.Minute);
        });
    ;
    var host = builder.Build();
    host.StartAsync();
    host.StopAsync();
    Console.WriteLine("Bye~~~");
}
catch (Exception ex)
{
    Log.Fatal(ex, "Host terminated unexpectedly");
    throw;
}
finally
{
    Log.CloseAndFlush();
}

 

LabBackgroundService 也有吃到 Serilog 的實例

public class LabBackgroundService : BackgroundService
{
    private readonly ILogger<LabBackgroundService> _logger;

    public LabBackgroundService(ILogger<LabBackgroundService> logger)
    {
        this._logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        this._logger.LogInformation(new EventId(2000, "Trace"), "Start {ClassName}.{MethodName}...",
            nameof(LabBackgroundService), nameof(this.ExecuteAsync));

        var sensorInput = new { Latitude = 25, Longitude = 134 };
        this._logger.LogInformation("Processing {@SensorInput}", sensorInput);
    }
}

 

Log 等級

在 Controller 注入 ILogger<T>,然後依照需求呼叫各個不同等級的 logging,Serilog 提供以下 Log

_logger.LogInformation("Info!");
_logger.LogWarning("Warning!");
_logger.LogTrace("Trace!");
_logger.LogDebug("Debug");
_logger.LogCritical("Critical");
_logger.LogError("Error");

 

Serilog 的 LogEventLevel 只有 6 個層級,分別是 

  • Verbose = 0
  • Debug = 1
  • Information = 2
  • Warning = 3
  • Error = 4
  • Fatal = 5

這跟微軟的  LogLevel 列舉 (Microsoft.Extensions.Logging) | Microsoft Docs 他有七個,對不太起來,Serilog 沒有 None,使用上要稍微注意一下,不要用錯了

 

Sinks

Serilog 提供了相當豐富的 Provided Sinks · serilog/serilog Wiki (github.com),這有點像是 NLog 的 Target

Serilog 寫入 Seq 

log 寫入本機檔案仍不易除錯,seq 是一套強大的 log server,支援結構化查詢

安裝
docker run --name seq -d --restart unless-stopped -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:latest

 

.WriteTo.File() 改成 .WriteTo.Seq("http://localhost:5341")

執行結果如下圖:

結構化資料

有關結構化相關知識的這裡可以參考這裡

Structured Data · serilog/serilog Wiki (github.com)

Structured logging concepts in .NET Series (1) (nblumhardt.com)

Message Templates

 

裡面有一個很關鍵的 Message Template,這跟我們在使用 string.Format() 的寫法很像,以下例,最後寫入到 log 的時候可以看到 ControllerName 和 MethodName 欄位

this._logger.LogInformation(new EventId(2000, "Trace"), "Start {ControllerName}.{MethodName}...",
    nameof(WeatherForecastController), nameof(Get));

 

執行結果如下圖,可以看到除了 Message 有完整的句子,ControllerName 和 MethodName 欄位被拆了出來

 

解構符

把一個物件攤平,使用 @ 解構操作符號

var sensorInput = new { Latitude = 25, Longitude = 134 };
Log.Information("Processing {@SensorInput}", sensorInput);

 

在 Console 應用程式輸出,SensorInput 相關的欄位被攤開,如下

Processing {"Latitude":25,"Longitude":134}

 

在 ASP.NET Core 應用程式輸出,SensorInput 相關的欄位被攤開,如下

{
 "Timestamp": "2022-09-04T12:20:30.9831700+08:00",
 "Level": "Information",
 "MessageTemplate": "Processing {@SensorInput}",
 "Properties": {
   "SensorInput": {
     "Latitude": 25,
     "Longitude": 134
   },
   "SourceContext": "Lab.SerilogProject.WebApi.Controllers.WeatherForecastController",
   "ActionId": "535101ad-6bd6-40de-a4af-7640c77f1d1b",
   "ActionName": "Lab.SerilogProject.WebApi.Controllers.WeatherForecastController.Get (Lab.SerilogProject.WebApi)",
   "RequestId": "0HMKE5J13R5K2:00000005",
   "RequestPath": "/WeatherForecast",
   "ConnectionId": "0HMKE5J13R5K2"
 }
}

 

Seq 呈現效果如下

 

輸出格式化

安裝

dotnet add package Serilog.Formatting.Compact --version 1.1.0

在 .WriteTo.Console(), Debug(), and File() 這些 Skins 支援 Json Format,這個功能在 Serilog.Formatting.Compact 套件,預設有以下

  • CompactJsonFormatter()
  • JsonFormatter()
  • MessageTemplateTextFormatter()
  • RawFormatter()
  • RenderedCompactJsonFormatter()

範例如下:

var formatter = new CompactJsonFormatter();
builder.Host.UseSerilog((context, services, config) =>
{
config.ReadFrom.Configuration(context.Configuration)
 .ReadFrom.Services(services)
 .Enrich.FromLogContext()
 .WriteTo.Console(formatter)
 .WriteTo.Seq("http://localhost:5341")
 .WriteTo.File(formatter, "logs/aspnet-.txt", rollingInterval: RollingInterval.Minute)
});

 

執行結果如下:

 

ExpressionTemplate

提供更豐富的輸出方式,比如,我想要把 @t 變成 _t

安裝

dotnet add package Serilog.Expressions --version 3.4.0

 

範例如下:

var formatter = new ExpressionTemplate(
    "{ {_t: @t, _msg: @m, _props: @p} }\n");

 

執行結果如下:

 

參考

Customized JSON formatting with Serilog (nblumhardt.com)

Formatting Output · serilog/serilog Wiki (github.com)

 

 

附加額外訊息

替每一個 Log 加上附加訊息

Microsoft.Extensions.Logging.ILogger.BeginScope

using var scope = this._logger.BeginScope(new Dictionary<string, object>
{
    ["UserId"] = "svrooij",
    ["OperationType"] = "update",
});

// UserId and OperationType are set for all logging events in these brackets
this._logger.LogInformation(new EventId(2000, "Trace"), "Start {ControllerName}.{MethodName}...",
    nameof(WeatherForecastController), nameof(Get));
    
var sensorInput = new { Latitude = 25, Longitude = 134 };
this._logger.LogInformation("Processing {@SensorInput}", sensorInput);

 

Serilog.Context.LogContext

using (LogContext.PushProperty("UserId","svrooij"))
using (LogContext.PushProperty("OperationType", "update"))
{
    
}

 

Serilog.LoggerConfiguration.Enrich.WithProperty

builder.Host.UseSerilog((context, services, config) =>
{
	config.ReadFrom.Configuration(context.Configuration)

        ...
		.Enrich.WithProperty("UserId","svrooij")
		.Enrich.WithProperty("OperationType","update")
		...
		;
});

 

Middleware

public class TraceMiddleware
{
    private readonly RequestDelegate _next;

    public TraceMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, ILogger<TraceMiddleware> logger)
    {
        using (logger.BeginScope(new Dictionary<string, object>
        {
            ["UserId"] = "svrooij",
            ["OperationType"] = "update",
        }))
        {
            await this._next.Invoke(context);
        }
    }
}

 

註冊 Middleware

app.UseMiddleware<TraceMiddleware>();

 

Seq 執行結果如下:

 

在每一個 Request 附加訊息

代碼如下:

app.UseSerilogRequestLogging(options =>
{
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("UserId", "svrooij");
        diagnosticContext.Set("OperationType", "update");
    };
}); 

 

Seq 執行結果如下:

 

範例位置

sample.dotblog/StructLog/Lab.SerilogProject at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo