写点什么

如何对 ILogger 进行扩展并实现日志分类及追踪

作者:多态丶
  • 2024-01-12
    江苏
  • 本文字数:3806 字

    阅读完需:约 12 分钟

如何对ILogger进行扩展并实现日志分类及追踪

在 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} 
<换行>
复制代码


而实际上我们想要的日志信息更丰富点,里面包含了modulecategroysubcategoryfilter1filter2traceId 可以提供给用户进行分类索引和追踪。例如,日志所属代码的业务模块、代码类、代码方法及关键参数(如订单号)等信息。以及提供一个traceId,可以根据 traceId 将事务链路的日志以串联时序的形式被检索出来。

{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}]  [{module}] [{category}] [{subcategory}] [{filter1}] [{filter2}] [{traceId}]  {Message}{NewLine}{Exception}  <换行>
复制代码


其实我们可以利用SerillogLogContextILogger增加一些扩展和承载更多的字段信息,来满足我们日常日志的需求。


如何进行扩展


首先引入 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的扩展和对利用SerilogLogContext功能代码的封装。通过少量的代码,可以让我们依然可以沿用官方接口。如注入:ILogger<TestService> logger 这样的代码来使用 ILogger ,而不是很粗鲁的自己写自己的什么 IMyLogger 。在此基础上,我们扩展了想要的字段,并保持了结构化日志的特性。

发布于: 刚刚阅读数: 8
用户头像

多态丶

关注

还未添加个人签名 2017-11-02 加入

还未添加个人简介

评论

发布
暂无评论
如何对ILogger进行扩展并实现日志分类及追踪_netcore_多态丶_InfoQ写作社区