ASP.NET Core 日志记录的艺术

在生产系统中调试问题时,良好的日志记录是绝对必要的。然而,知道哪些信息可能有用,以及多少信息是过多的,几乎是一门艺术。最近,有机会在将基于ASP.NET Boilerplate的系统发布到生产环境时发展这门艺术。总体部署进展顺利,但意识到没有在日志中包含当前登录的用户或租户信息,这使得调试问题变得更加困难。

因此,这是一个关于如何在ASP.NET Core中添加有关当前登录用户的信息,可能包括在多租户解决方案中的租户信息的故事。首先,将展示如何在ASP.NET Boilerplate与log4net中实现。接下来,将展示如何在ABP框架和Microsoft日志框架通过Serilog实现。希望能在这里找到一些可以适应技术栈的东西,并帮助发展内心的日志艺术。

ASP.NET Boilerplate +log4net

对于log4net,第一个技巧是添加自定义字段。这发生在log4net.config配置文件中。通过log4net的property{name}语法,这有点不太直观:

<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> ... <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%-5level %date [%-5.5property{userid}] [%-5.5property{tenantid}] [%-5.5thread] %-40.40logger - %message%newline"/> </layout> </appender>

将userid和tenantid字段用括号包围,并使用固定宽度的模式布局-5.5,以将小于5个字符的整数值填充到5个字符。

要在log4net中填充这些字段,需要在上下文中设置属性,这使得它对所有日志都可用。有四个上下文可供选择,但在这里最有意义的是逻辑线程上下文,因为这是处理请求的级别,并且即使使用不同的线程来恢复请求,它也可以跨await点持久存在。代码看起来像这样:

LogicalThreadContext.Properties["userid"] = ??

但是在哪里设置它呢?最合适的地方是在请求管道中的中间件组件中,正好在认证之后,以便当前用户可用。换句话说:

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { ... app.UseAuthentication(); app.UseSessionLogging(); ... }

UseSessionLogging是一个扩展方法,看起来像这样:

public static class SessionLoggingMiddlewareUtil { public static void UseSessionLogging(this IApplicationBuilder app) { app.UseMiddleware<SessionLoggingMiddleware>(); } }

选择了ASP.NET Core的工厂基础中间件激活,以便可以进行依赖注入,以便访问IAbpSession,可以获取当前用户和租户。那么,最后一块拼图就是:

public class SessionLoggingMiddleware : IMiddleware, ITransientDependency { private readonly IAbpSession _session; public SessionLoggingMiddleware(IAbpSession session) { _session = session; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { LogicalThreadContext.Properties["userid"] = _session.UserId; LogicalThreadContext.Properties["tenantid"] = _session.TenantId; await next(context); } }

完整的代码可以在LeesStore PR #30中找到。

请注意,使用其他appender,如ApplicationInsightsAppender,是类似的,只是有一些小的变化。

<appender name="AiAppender" type="Microsoft.ApplicationInsights.Log4NetAppender.ApplicationInsightsAppender, Microsoft.ApplicationInsights.Log4NetAppender"> <threshold value="Info"/> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%-5level %property{userid} %property{tenantid} %message%newline"/> </layout> </appender>

AdoNetAppender稍微棘手一些:

<appender name="AdoNetAppender" type="MicroKnights.Logging.AdoNetAppender, MicroKnights.Log4NetAdoNetAppender"> ... <commandText value="INSERT INTO LogEntries ([Date],[Level],[Logger],[Message],[Exception],[UserId],[TenantId]) VALUES (@log_date, @log_level, @logger, @message, @exception, @userid, @tenantid)"/> ... <parameter> <parameterName value="@userid"/> <dbType value="Int32"/> <layout type="log4net.Layout.RawPropertyLayout"> <key value="auserid"/> </layout> </parameter> </appender>

ABP Framework +Serilog

喜欢log4net。一直在用它。但是serilog更现代,感觉更优雅。使用它的等效自定义字段对于“控制台appender”来说就像在Program.cs中设置输出模板中的大括号一样简单:

Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Async(c => c.File("Logs/logs.txt", outputTemplate: "{Level:u4} [{UserId}] [{TenantId}] [{Timestamp:HH:mm:ss}] {Message:lj}{NewLine}{Exception}")) .WriteTo.Async(c => c.Console()) .CreateLogger();

设置这些自定义字段是通过LogContext.PushProperty完成的。放置代码的位置有点棘手。仍然是自定义中间件组件的粉丝,但是在ABP框架中插入中间件组件并不发生在Startup.cs中。这是因为ABP框架分散了逻辑,允许每个依赖模块注册自定义中间件。

但是,不需要自定义模块。就像以前一样添加中间件组件,但是在[MyProject]ApiHostModule的OnApplicationInitialization()方法中。

public override void OnApplicationInitialization(ApplicationInitializationContext context) { ... app.UseAuthentication(); app.UseSessionLogging(); }

然后中间件组件本身与上一个非常相似:

public class SessionLoggingMiddleware : IMiddleware, ITransientDependency { private readonly ICurrentUser _currentUser; private readonly ICurrentTenant _currentTenant; public SessionLoggingMiddleware(ICurrentUser currentUser, ICurrentTenant currentTenant) { _currentUser = currentUser; _currentTenant = currentTenant; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { using (Serilog.Context.LogContext.PushProperty("UserId", _currentUser.Id)) using (Serilog.Context.LogContext.PushProperty("TenantId", _currentTenant.Id)) { await next(context); } } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485