在 asp.net 核心中使用 serilog 维护单独的范围上下文
Maintaining separate scope context with serilog in asp.net core
我遇到了 ILogger<T>
个实例之间共享作用域上下文的问题。
下面是我如何在我的 asp.net 核心 3.1 服务中配置 Serilog 并注入 ILogger<T>
。我有一个 WithClassContext
扩展,我在每个 class 的构造函数中调用它,以便将 class 名称作为上下文 属性 推送。我发现错误的 ClassName
值出现在我的日志中。
当我在调试器中检查注入的 ILogger 对象时,我发现以下内容:
ILogger<T> -> Logger<T>
{
_logger -> Serilog.Extensions.Logger.SerilogLogger
{
_logger -> Serilog.Core.Logger
_provider -> Serilog.Extensions.Logger.SerilogLoggerProvider
{
CurrentScope -> Serilog.Extensions.Logger.SerilogLoggerScope
{
Parent -> Serilog.Extensions.Logger.SerilogLoggerScope
_state -> Dictionary<string, object>
}
}
}
}
所以我观察到的是每个注入的 ILogger<T>
都具有所有记录器共享的相同 _provider
对象。 _provider.CurrentScope
似乎是 SerilogLoggerScope
个对象的链表,_provider.CurrentScope
指向列表中的最后一个节点,_provider.CurrentScope.Parent
是前一个节点。还有一个 CurrentScope._state
Dictionary<string, object>
包含 属性 名称和值。写出范围上下文时,如果有任何冲突的 属性 名称,则使用列表中的最后一个 SerilogLoggerScope
。
所以使用下面的示例:
FooService
已创建并推送 ClassName
CurrentScope._state -> {"ClassName", "FooService"}
FooController
已创建并推送 ClassName
CurrentScope._state -> {"ClassName", "FooController"}
CurrentScope.Parent._state -> {"ClassName", "FooService"}
FooService: CurrentScope
现在与 FooController
相同。
所有 classes 的日志现在推送 "ClassName": "FooController"
.
我本以为通过注入ILogger<T>
,作用域上下文不会被其他实例共享。我还研究了其他人如何将 class 名称推送到日志记录上下文中,我相信我也在做同样的事情。
Program.cs:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog(ConfigureLogger)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseUrls("http://*:6050");
});
private static void ConfigureLogger(HostBuilderContext ctx, IServiceProvider sp, LoggerConfiguration config)
{
var shouldFormatElastic = !ctx.HostingEnvironment.EnvironmentName.Equals("local", StringComparison.OrdinalIgnoreCase);
config.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithExceptionDetails();
if (shouldFormatElastic)
{
var logFormatter = new ExceptionAsObjectJsonFormatter(renderMessage: true);
config.WriteTo.Async(a =>
a.Console(logFormatter, standardErrorFromLevel: LogEventLevel.Error));
}
else
{
config.WriteTo.Async(a =>
a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Properties:j}{Exception}{NewLine}",
theme: SystemConsoleTheme.Literate));
}
}
控制器:
[ApiController]
[Route("api/[controller]")]
public class FooController : ControllerBase
{
private ILogger _logger;
private readonly IFooService _fooService;
/// <inheritdoc />
public FooController(ILogger<FooController> logger, IFooService fooService)
{
_logger = logger.WithClassContext();
_fooService = fooService;
}
}
服务:
public class FooService : IFooService
{
private readonly ILogger _logger;
public FooService(ILogger<IFooService> logger)
{
_logger = logger.WithClassContext();
}
LoggingExtensions.cs
public static class FooLoggingExtensions
{
public static ILogger Here(this ILogger logger, [CallerMemberName] string memberName = null)
{
var state = new Dictionary<string, object>
{
{"MemberName", memberName }
};
// Don't need to dispose since this scope will last through the call stack.
logger.BeginScope(state);
return logger;
}
public static ILogger WithClassContext<T>(this ILogger<T> logger)
{
var state = new Dictionary<string, object>
{
{"ClassName", typeof(T).Name }
};
// Don't need to dispose since this scope will last through the call stack.
logger.BeginScope(state);
return logger;
}
}
根据您的尝试,BeginScope 似乎不是解决问题的正确工具。您要解决的问题是每个日志都需要将 ClassName 作为日志消息的一部分。为此,您可以修改您使用的 outputTemplate 以包含 {SourceContext}
,然后在构造函数内部而不是调用 logger.WithClassContext()
,而是调用 logger.ForContext<ClassName>()
.
请注意,我在下面的例子中只修改了本地环境日志。
private static void ConfigureLogger(HostBuilderContext ctx, IServiceProvider sp, LoggerConfiguration config)
{
var shouldFormatElastic = !ctx.HostingEnvironment.EnvironmentName.Equals("local", StringComparison.OrdinalIgnoreCase);
config.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithExceptionDetails();
if (shouldFormatElastic)
{
var logFormatter = new ExceptionAsObjectJsonFormatter(renderMessage: true);
config.WriteTo.Async(a =>
a.Console(logFormatter, standardErrorFromLevel: LogEventLevel.Error));
}
else
{
config.WriteTo.Async(a =>
a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Properties:j}{Exception}{NewLine}",
theme: SystemConsoleTheme.Literate));
}
}
[ApiController]
[Route("api/[controller]")]
public class FooController : ControllerBase
{
private ILogger _logger;
private readonly IFooService _fooService;
/// <inheritdoc />
public FooController(ILogger<FooController> logger, IFooService fooService)
{
_logger = logger.ForContext<FooController>();
_fooService = fooService;
}
}
我使用以下 sites/blogs 作为参考来制定这个答案。
https://benfoster.io/blog/serilog-best-practices/#source-context
https://github.com/serilog/serilog/issues/968
我遇到了 ILogger<T>
个实例之间共享作用域上下文的问题。
下面是我如何在我的 asp.net 核心 3.1 服务中配置 Serilog 并注入 ILogger<T>
。我有一个 WithClassContext
扩展,我在每个 class 的构造函数中调用它,以便将 class 名称作为上下文 属性 推送。我发现错误的 ClassName
值出现在我的日志中。
当我在调试器中检查注入的 ILogger 对象时,我发现以下内容:
ILogger<T> -> Logger<T>
{
_logger -> Serilog.Extensions.Logger.SerilogLogger
{
_logger -> Serilog.Core.Logger
_provider -> Serilog.Extensions.Logger.SerilogLoggerProvider
{
CurrentScope -> Serilog.Extensions.Logger.SerilogLoggerScope
{
Parent -> Serilog.Extensions.Logger.SerilogLoggerScope
_state -> Dictionary<string, object>
}
}
}
}
所以我观察到的是每个注入的 ILogger<T>
都具有所有记录器共享的相同 _provider
对象。 _provider.CurrentScope
似乎是 SerilogLoggerScope
个对象的链表,_provider.CurrentScope
指向列表中的最后一个节点,_provider.CurrentScope.Parent
是前一个节点。还有一个 CurrentScope._state
Dictionary<string, object>
包含 属性 名称和值。写出范围上下文时,如果有任何冲突的 属性 名称,则使用列表中的最后一个 SerilogLoggerScope
。
所以使用下面的示例:
FooService
已创建并推送ClassName
CurrentScope._state -> {"ClassName", "FooService"}
FooController
已创建并推送ClassName
CurrentScope._state -> {"ClassName", "FooController"}
CurrentScope.Parent._state -> {"ClassName", "FooService"}
FooService:
CurrentScope
现在与FooController
相同。所有 classes 的日志现在推送
"ClassName": "FooController"
.
我本以为通过注入ILogger<T>
,作用域上下文不会被其他实例共享。我还研究了其他人如何将 class 名称推送到日志记录上下文中,我相信我也在做同样的事情。
Program.cs:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog(ConfigureLogger)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseUrls("http://*:6050");
});
private static void ConfigureLogger(HostBuilderContext ctx, IServiceProvider sp, LoggerConfiguration config)
{
var shouldFormatElastic = !ctx.HostingEnvironment.EnvironmentName.Equals("local", StringComparison.OrdinalIgnoreCase);
config.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithExceptionDetails();
if (shouldFormatElastic)
{
var logFormatter = new ExceptionAsObjectJsonFormatter(renderMessage: true);
config.WriteTo.Async(a =>
a.Console(logFormatter, standardErrorFromLevel: LogEventLevel.Error));
}
else
{
config.WriteTo.Async(a =>
a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Properties:j}{Exception}{NewLine}",
theme: SystemConsoleTheme.Literate));
}
}
控制器:
[ApiController]
[Route("api/[controller]")]
public class FooController : ControllerBase
{
private ILogger _logger;
private readonly IFooService _fooService;
/// <inheritdoc />
public FooController(ILogger<FooController> logger, IFooService fooService)
{
_logger = logger.WithClassContext();
_fooService = fooService;
}
}
服务:
public class FooService : IFooService
{
private readonly ILogger _logger;
public FooService(ILogger<IFooService> logger)
{
_logger = logger.WithClassContext();
}
LoggingExtensions.cs
public static class FooLoggingExtensions
{
public static ILogger Here(this ILogger logger, [CallerMemberName] string memberName = null)
{
var state = new Dictionary<string, object>
{
{"MemberName", memberName }
};
// Don't need to dispose since this scope will last through the call stack.
logger.BeginScope(state);
return logger;
}
public static ILogger WithClassContext<T>(this ILogger<T> logger)
{
var state = new Dictionary<string, object>
{
{"ClassName", typeof(T).Name }
};
// Don't need to dispose since this scope will last through the call stack.
logger.BeginScope(state);
return logger;
}
}
根据您的尝试,BeginScope 似乎不是解决问题的正确工具。您要解决的问题是每个日志都需要将 ClassName 作为日志消息的一部分。为此,您可以修改您使用的 outputTemplate 以包含 {SourceContext}
,然后在构造函数内部而不是调用 logger.WithClassContext()
,而是调用 logger.ForContext<ClassName>()
.
请注意,我在下面的例子中只修改了本地环境日志。
private static void ConfigureLogger(HostBuilderContext ctx, IServiceProvider sp, LoggerConfiguration config)
{
var shouldFormatElastic = !ctx.HostingEnvironment.EnvironmentName.Equals("local", StringComparison.OrdinalIgnoreCase);
config.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithExceptionDetails();
if (shouldFormatElastic)
{
var logFormatter = new ExceptionAsObjectJsonFormatter(renderMessage: true);
config.WriteTo.Async(a =>
a.Console(logFormatter, standardErrorFromLevel: LogEventLevel.Error));
}
else
{
config.WriteTo.Async(a =>
a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Properties:j}{Exception}{NewLine}",
theme: SystemConsoleTheme.Literate));
}
}
[ApiController]
[Route("api/[controller]")]
public class FooController : ControllerBase
{
private ILogger _logger;
private readonly IFooService _fooService;
/// <inheritdoc />
public FooController(ILogger<FooController> logger, IFooService fooService)
{
_logger = logger.ForContext<FooController>();
_fooService = fooService;
}
}
我使用以下 sites/blogs 作为参考来制定这个答案。
https://benfoster.io/blog/serilog-best-practices/#source-context
https://github.com/serilog/serilog/issues/968