一、前言
在上一章節中使用NLog+SEQ+ILogger構建視覺化日誌,本章基於Serilog來構建視覺化日誌。Serilog是.NET中一款非常出色的結構化日誌的日誌庫,其與NLog對比在前一章節進行了說明。Serilog專案不僅僅包含核心專案Seirlog,而且還提供了很多接收器Sinks(超過100個),這些接收器是透過外掛的方式來實現將日誌寫入到各種終端、檔案、郵件、資料庫或日誌伺服器。
二、實踐
第一步、安裝SEQ服務,並建立Serilog所需要的ApiKey。
第二步:建立一個ASP.NET Core的應用,引入相關的包,如下所示。
<ItemGroup> <PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.0" /> <PackageReference Include="Serilog.Expressions" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" /> </ItemGroup>
上述安裝包主要包括Serilog(基礎包),Serilog.AspNetCore(ASP.NET Core應用包),Serilog.Enrichers.Environment(將系統環境資訊為日誌事件屬性記錄到日誌),並且提供豐富的Enrichers記錄詳細的日誌資訊,Serilog.Sinks.Async,Serilog.Sinks.Console,Serilog.Sinks.Seq都是Serilog非同步記錄寫入日誌檔案、寫入控制檯列印、非同步寫入Seq等接收器,Serilog提供豐富的接收器。
第三步、完成上述操作,下一步在程式碼中實現日誌的收集、儲存、驗證查詢功能,日誌包含一般列印日誌、介面請求日誌,所以要對ILogger使用一般日誌,介面請求日誌進行處理,對於一般列印的日誌在上一章節介紹Serilog已經實踐了(映入安裝包、定義Log配置、注入Serilog的服務,然後在程式碼中使用ILogger記錄日誌),對於請求日誌則要使用中介軟體對請求進行處理(定義一個過濾器對介面請求Action相關上下文處理、定義一個特性對這個過濾器在Action方法上標記)。
1、注入Serilog服務,定義一個服務擴充套件類,減少全部程式碼堆積在Program中,類名稱為ServiceCollectionExtensions,並且使用Serilog的Console日誌、檔案日誌、Seq日誌儲存程式碼如下。
using Serilog;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
namespace tqf.serilogSeq.Demo
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddConfigSerilog(this IServiceCollection services, IConfiguration configuration)
{
var seqApiKey = configuration["seq:apiKey"].ToString();
var seqServerUrl = configuration["seq:serverUrl"].ToString();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Debug)
.MinimumLevel.Override("Microsoft", LogEventLevel.Debug)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Error)
.MinimumLevel.Override("Microsoft.AspNetCore.Cors.Infrastructure.CorsService", LogEventLevel.Error)
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Error)
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Error)
// 全部日誌寫入到Console
.WriteTo.Async(c=>c.Console(theme:AnsiConsoleTheme.Literate,
outputTemplate: "[T] {Timestamp:yyyy-MM-dd HH:mm:ss,fff} {MachineName}{NewLine}[L] [{Level}]{NewLine}[C] [{SourceContext}] {Message:lj}{NewLine}{Exception}{NewLine}"))
// Information日誌寫入到檔案
.WriteTo.Async(c => c.File(
path: "Logs/Information/.txt",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 1L * 1024 * 1024 * 1024,
retainedFileCountLimit: 31,
outputTemplate: "[T] {Timestamp:yyyy-MM-dd HH:mm:ss,fff} {MachineName}{NewLine}[L] [{Level}]{NewLine}[C] [{SourceContext}] {Message:lj}{NewLine}{Exception}{NewLine}",
restrictedToMinimumLevel: LogEventLevel.Information))
// Debug日誌寫入到檔案
.WriteTo.Async(c => c.File(
path: "Logs/Debug/.txt",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 1L * 1024 * 1024 * 1024,
retainedFileCountLimit: 31,
outputTemplate: "[T] {Timestamp:yyyy-MM-dd HH:mm:ss,fff} {MachineName}{NewLine}[L] [{Level}]{NewLine}[C] [{SourceContext}] {Message:lj}{NewLine}{Exception}{NewLine}",
restrictedToMinimumLevel: LogEventLevel.Debug))
// Warning日誌寫入到檔案
.WriteTo.Async(c => c.File(
path: "Logs/Warning/.txt",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 1L * 1024 * 1024 * 1024,
retainedFileCountLimit: 31,
outputTemplate: "[T] {Timestamp:yyyy-MM-dd HH:mm:ss,fff} {MachineName}{NewLine}[L] [{Level}]{NewLine}[C] [{SourceContext}] {Message:lj}{NewLine}{Exception}{NewLine}",
restrictedToMinimumLevel: LogEventLevel.Warning))
// Error日誌寫入到檔案
.WriteTo.Async(c => c.File(
path: "Logs/Error/.txt",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 1L * 1024 * 1024 * 1024,
retainedFileCountLimit: 31,
outputTemplate: "[T] {Timestamp:yyyy-MM-dd HH:mm:ss,fff} {MachineName}{NewLine}[L] [{Level}]{NewLine}[C] [{SourceContext}] {Message:lj}{NewLine}{Exception}{NewLine}",
restrictedToMinimumLevel: LogEventLevel.Error))
// 全部日誌寫入到Seq
.WriteTo.Async(c=>c.Seq(apiKey:seqApiKey,serverUrl:seqServerUrl))
.Enrich.FromLogContext()
.Enrich.WithMachineName()
// 過濾請求:/health心跳
.Filter.ByExcluding("RequestPath like '/health%'")
.CreateBootstrapLogger();
services.AddSerilog();
return services;
}
}
}
2、啟用中介軟體,定義一個ApplicationBuilder擴充套件類,減少中介軟體程式碼堆積在Program中,類名稱ApplicationBuilderExtensions,程式碼如下。
using Serilog; using Serilog.Events; using System.Net; namespace tqf.serilogSeq.Demo { /// <summary> /// ApplicationBuilder 擴充套件 /// </summary> public static class ApplicationBuilderExtensions { /// <summary> /// 新增使用Serilog請求日誌 /// </summary> /// <param name="app"></param> /// <returns></returns> public static IApplicationBuilder UseSerilogRequestLogging(this IApplicationBuilder app) { //允許body重用 app.Use(next => context => { context.Request.EnableBuffering(); return next(context); }); // 新增使用Serilog記錄請求日誌 app.UseSerilogRequestLogging(options => { // 請求日誌輸出模板 options.MessageTemplate = "\n[R] {RequestMethod}={_RequestPath} | Status={StatusCode} | Time={Elapsed}ms\n[R] Req={_RequestBody} | Res={_ResponseBody}"; // 發出除錯級別事件而不是預設事件,將請求日誌記錄到:Debug日誌 options.GetLevel = (httpContext, elapsed, ex) => LogEventLevel.Debug; // 將其他屬性附加到請求完成事件,將請求屬性附加可以在模板中使用 options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { diagnosticContext.Set("_RequestPath", WebUtility.UrlDecode(httpContext.Request.Path + httpContext.Request.QueryString)); //請求body var requestContent = ""; var method = httpContext.Request.Method.ToLower(); if (method == "post" || method == "put") { httpContext.Request.Body.Position = 0; var requestReader = new StreamReader(httpContext.Request.Body); requestContent = requestReader.ReadToEnd(); } diagnosticContext.Set("_RequestBody", requestContent); diagnosticContext.Set("_Service", AppDomain.CurrentDomain.FriendlyName); }; }); return app; } } }
3、定義一個Filter類,對Action的請求進行過濾操作,類名稱RequestAuditLogFilter,程式碼如下。
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; using Serilog; using Newtonsoft.Json; namespace tqf.serilogSeq.Demo { /// <summary> /// 請求過濾器 /// </summary> public class RequestAuditLogFilter : IResultFilter { private readonly IDiagnosticContext _diagnosticContext; public RequestAuditLogFilter(IDiagnosticContext diagnosticContext) { _diagnosticContext = diagnosticContext; } public void OnResultExecuted(ResultExecutedContext context) { var result = context.Result as ObjectResult; var resultJson = JsonConvert.SerializeObject(result?.Value); _diagnosticContext.Set("_ResponseBody", resultJson); } public void OnResultExecuting(ResultExecutingContext context) { } } }
4、定義一個Attribute,實現過濾器標記在Action方法上,程式碼如下。
using Microsoft.AspNetCore.Mvc; namespace tqf.serilogSeq.Demo { /// <summary> /// 請求特性,標籤要記錄請求日誌 /// </summary> public class RequestAuditLogAttribute : TypeFilterAttribute { public RequestAuditLogAttribute() : base(typeof(RequestAuditLogFilter)) { } } }
5、在Program中使用擴充套件的ServiceCollection與ApplicationBuilder來注入Serilog的服務,啟用Serilog的中介軟體,程式碼如下。
using Microsoft.Extensions.Configuration; namespace tqf.serilogSeq.Demo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // 使用Serilog記錄日誌 builder.Services.AddConfigSerilog(configuration: builder.Configuration); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseAuthorization(); // 使用Serilog記錄請求日誌 app.UseSerilogRequestLogging(); app.MapControllers(); app.Run(); } } }
6、在業務程式碼中使用日誌,程式碼如下,注意注入IDiagnosticContext例項,然後在Action新增定義的特性RequestAuditLog記錄請求日誌。
using Microsoft.AspNetCore.Mvc; using Serilog; namespace tqf.serilogSeq.Demo.Controllers { /// <summary> /// 控制器 /// </summary> [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; private readonly IDiagnosticContext _diagnosticContext; public WeatherForecastController(ILogger<WeatherForecastController> logger, IDiagnosticContext diagnosticContext) { _logger = logger; _diagnosticContext = diagnosticContext; } [HttpGet("LogInformation")] [RequestAuditLog] public string LogInformation() { return "ok"; } [HttpGet("LogError")] [RequestAuditLog] public string LogError() { int a = 10; int b = 0; int c = a / b; return "ok"; } [HttpPost("LogRequest")] [RequestAuditLog] public List<string> LogRequest([FromBody] List<string> input) { for (int i = 0; i < 10; i++) { input.Add(Guid.NewGuid().ToString()); } return input; } [HttpGet] [RequestAuditLog] public IEnumerable<WeatherForecast> Get() { 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(); } } }
7、在appsetting中配置上述的Seq的地址與Key資訊。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Seq": {
"apiKey": "GgnrgKNNuwBsP1JHbPLb",
"serverUrl": "http://localhost:5341"
},
"AllowedHosts": "*"
}
第四步、校驗結果,執行程式,透過檢視Console、檔案、SEQ是否儲存相關日誌資料,及其資料格式項。
如上所示,在三個型別的接收器都接受到相關的資料型別和格式資訊,很好完成日誌記錄的功能。
三、總結
透過上述的實踐,Serilog+SEQ+ILogger,提供高可擴充套件、高靈活性的日誌記錄服務功能,並且透過上述的擴充套件類,過濾器、特性、配置項的方式,提供一個低程式碼入侵的思想,對於日誌管理真正最佳化的實現了。
參考:https://www.cnblogs.com/heyangyi/p/18082599,https://www.cnblogs.com/RainFate/p/16869403.html