在 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.dll
2024-01-12 15:58:39.851 +08:00 [Information] [] [] [Get] [] [] [] Before Get
2024-01-12 15:58:39.869 +08:00 [Information] [ClassA] [MethodB] [Get] [A1] [B1] [34b6c688-ad5c-4ee9-94e4-52b567b6e2a5] In Get Step1
2024-01-12 15:58:39.869 +08:00 [Information] [ClassA] [MethodB] [Get] [A2] [B2] [34b6c688-ad5c-4ee9-94e4-52b567b6e2a5] In Get Step2
2024-01-12 15:58:39.869 +08:00 [Information] [ClassA] [MethodB] [Get] [A3] [B3] [34b6c688-ad5c-4ee9-94e4-52b567b6e2a5] In Get Step3
2024-01-12 15:58:39.869 +08:00 [Information] [] [] [DoSomething] [A3] [B3] [] In Get Step3
2024-01-12 15:58:39.869 +08:00 [Information] [] [] [Get] [] [] [] After Get
复制代码
总结
本文通过一个 LoggerRichExtensions
类 ,完成了对 ILogger
的扩展和对利用Serilog
的LogContext
功能代码的封装。通过少量的代码,可以让我们依然可以沿用官方接口。如注入:ILogger<TestService> logger
这样的代码来使用 ILogger
,而不是很粗鲁的自己写自己的什么 IMyLogger
。在此基础上,我们扩展了想要的字段,并保持了结构化日志的特性。
评论