在生产系统中调试问题时,良好的日志记录是绝对必要的。然而,知道哪些信息可能有用,以及多少信息是过多的,几乎是一门艺术。最近,有机会在将基于ASP.NET Boilerplate的系统发布到生产环境时发展这门艺术。总体部署进展顺利,但意识到没有在日志中包含当前登录的用户或租户信息,这使得调试问题变得更加困难。
因此,这是一个关于如何在ASP.NET Core中添加有关当前登录用户的信息,可能包括在多租户解决方案中的租户信息的故事。首先,将展示如何在ASP.NET Boilerplate与log4net中实现。接下来,将展示如何在ABP框架和Microsoft日志框架通过Serilog实现。希望能在这里找到一些可以适应技术栈的东西,并帮助发展内心的日志艺术。
对于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>
喜欢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);
}
}
}