在 Asp.NetCore 使用ILogger的过程中,官方的ILogger默认的扩展方法的参数好像非常少。只有 message 和 args 参数。那么我们改如何进行日志分类呢,比如日志的模块、类、方法及参数等信息,以及包括事务链路追踪的信息,让日志可以串联起来。
public static class LoggerExtensions{ //官方给的扩展,只有message和args参数,对log方法进行封装。(ps暂不考虑eventId) public static void LogInformation(this ILogger logger, string? message, params object?[] args) { logger.Log(LogLevel.Information, message, args); } //...}
复制代码
默认参数不够,导致了我们的模版好像并没有什么可以填充的。如下:
{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}
<换行>
复制代码
而实际上我们想要的日志信息更丰富点,里面包含了module,categroy,subcategory,filter1,filter2,traceId 可以提供给用户进行分类索引和追踪。例如,日志所属代码的业务模块、代码类、代码方法及关键参数(如订单号)等信息。以及提供一个traceId,可以根据 traceId 将事务链路的日志以串联时序的形式被检索出来。
{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{module}] [{category}] [{subcategory}] [{filter1}] [{filter2}] [{traceId}] {Message}{NewLine}{Exception} <换行>
复制代码
其实我们可以利用Serillog的LogContext对ILogger增加一些扩展和承载更多的字段信息,来满足我们日常日志的需求。
如何进行扩展
首先引入 Serilog,并设定日志模版:
var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{module}] [{category}] [{subcategory}] [{filter1}] [{filter2}] [{traceId}] {Message}{NewLine}{Exception}") .CreateLogger();
复制代码
然后对ILogger进行一些扩展,扩展内部进行封装一些代码。原理是我们将自己想要的字段放在LogContext ,最终日志模块在利用以上的模版输出日志到文本文件的时候,就会去取LogContext里面的字段值。如下:
public static class LoggerRichExtensions{ public static void LogInformation(this ILogger logger, string message, string module = "", string category = "", [CallerMemberName] string subcategory = "", string filter1 = "", string filter2 = "", string traceId = "", params object?[] args) { const bool destructureObjects = false; var enriches = new List<PropertyEnricher>(); if (!string.IsNullOrWhiteSpace(module)) { enriches.Add(new PropertyEnricher("module", module, destructureObjects)); } if (!string.IsNullOrWhiteSpace(category)) { enriches.Add(new PropertyEnricher("category", category, destructureObjects)); } if (!string.IsNullOrWhiteSpace(subcategory)) { enriches.Add(new PropertyEnricher("subcategory", subcategory, destructureObjects)); } if (!string.IsNullOrWhiteSpace(filter1)) { enriches.Add(new PropertyEnricher("filter1", filter1, destructureObjects)); } if (!string.IsNullOrWhiteSpace(filter2)) { enriches.Add(new PropertyEnricher("filter2", filter2, destructureObjects)); } if (!string.IsNullOrWhiteSpace(traceId)) { enriches.Add(new PropertyEnricher("traceId", traceId, destructureObjects)); } if (enriches.Any()) { // 在LogContext被释放之前的日志中,都会携带以上字段信息。 using (LogContext.Push(enriches.ToArray())) { logger.Log(LogLevel.Information, message, args); } } else { logger.Log(LogLevel.Information, message, args); } }}
复制代码
另外,其实大家在平时写日志的时候,如果用了日志的 “message 模版 + args 参数” 的方式,那么 messsga 里面占位符和 args 里面的具体值,也会被放到LogContext里面去。其中 message 不会立即进行和 args 进行填充,只是在最终输出到 log 文件的时候才填充。这也从另外一个侧面解释了用 message + args 字段的方式会对业务代码减少更多的性能消耗。
// 性能侵入低, 日志结构化程度高,最终也会在LogContext中存在一个KV数据: {"name":"Liu,DeHua""}logger.LogInformation("hello,{name}", args: "Liu,DeHua");
// 性能侵入高,日志结构化程度低,不存在KV数据var name = "Liu,DeHua";logger.LogInformation($"Hello,{name}");
复制代码
如何进行日志追踪
在日志可以被分类的基础上,我们还可以利用logger.BeginScope 的方法对日志进行 scope化 ,共享同一个日志实例。
public static class LoggerRichExtensions{ public static IDisposable? BeginScope(this ILogger logger, string module = "", string category = "", [CallerMemberName] string subcategory = "", string filter1 = "", string filter2 = "", string traceId = "") { Dictionary<string, object> state = new(); if (!string.IsNullOrWhiteSpace(module)) { state.Add("module", module); }
if (!string.IsNullOrWhiteSpace(category)) { state.Add("category", category); }
if (!string.IsNullOrWhiteSpace(subcategory)) { state.Add("subcategory", subcategory); }
if (!string.IsNullOrWhiteSpace(filter1)) { state.Add("filter1", filter1); }
if (!string.IsNullOrWhiteSpace(filter2)) { state.Add("filter2", filter2); }
if (!string.IsNullOrWhiteSpace(traceId)) { state.Add("traceId", traceId); }
return logger.BeginScope(state); }}
复制代码
底层 logger.BeginScope 的参数要求是一个泛型类型。如上面的代码,我们使用了 Dictionary<string,object> 类型进行传递。那么这些数据会被放在一个共享的 LogContext 中,并不可以被修改。
[Route("api/test")]public class TestController(ILogger<TestController> logger, TestService testService) : ControllerBase{ [HttpGet] public ActionResult Get() { logger.LogInformation("Before Get"); using (logger.BeginScope(module: "ClassA", category: "MethodB", traceId: Guid.NewGuid().ToString())) { // 在此范围内的日志都会被添加上 module, category, traceId 这三个属性。而且不可以被修改。 logger.LogInformation("In Get Step1", filter1: "A1", filter2: "B1"); logger.LogInformation("In Get Step2", filter1: "A2", filter2: "B2"); testService.DoSomething(); } testService.DoSomething(); logger.LogInformation("After Get"); return Ok(); }}
public class TestService(ILogger<TestService> logger){ public void DoSomething() { logger.LogInformation("In Get Step3", filter1: "A3", filter2: "B3"); }}
复制代码
/usr/local/share/dotnet/dotnet /Users/fenghui.xu/SourceCode/个人代码/TestSln/LoggerTest/LoggerTest/bin/Debug/net8.0/LoggerTest.dll2024-01-12 15:58:39.851 +08:00 [Information] [] [] [Get] [] [] [] Before Get2024-01-12 15:58:39.869 +08:00 [Information] [ClassA] [MethodB] [Get] [A1] [B1] [34b6c688-ad5c-4ee9-94e4-52b567b6e2a5] In Get Step12024-01-12 15:58:39.869 +08:00 [Information] [ClassA] [MethodB] [Get] [A2] [B2] [34b6c688-ad5c-4ee9-94e4-52b567b6e2a5] In Get Step22024-01-12 15:58:39.869 +08:00 [Information] [ClassA] [MethodB] [Get] [A3] [B3] [34b6c688-ad5c-4ee9-94e4-52b567b6e2a5] In Get Step32024-01-12 15:58:39.869 +08:00 [Information] [] [] [DoSomething] [A3] [B3] [] In Get Step32024-01-12 15:58:39.869 +08:00 [Information] [] [] [Get] [] [] [] After Get
复制代码
总结
本文通过一个 LoggerRichExtensions 类 ,完成了对 ILogger的扩展和对利用Serilog的LogContext功能代码的封装。通过少量的代码,可以让我们依然可以沿用官方接口。如注入:ILogger<TestService> logger 这样的代码来使用 ILogger ,而不是很粗鲁的自己写自己的什么 IMyLogger 。在此基础上,我们扩展了想要的字段,并保持了结构化日志的特性。
评论