Serilog .ForContext 无法通过 service(/worker) 类使用

Geo*_*ope 5 c# dependency-injection serilog .net-6.0

如何修复依赖项注入,以便可以.ForContext(...)在我的工作人员/服务类中进行访问?

[.Net Core 6 用于混合控制台/WindowsService 应用程序]

在我的主程序类和工作/服务类中,我让 Serilog 正确工作以进行基本日志记录...

程序.cs

Log.Information($"Application Directory: {baseDir}");
Run Code Online (Sandbox Code Playgroud)

工人.cs

_logger.LogInformation("Service Starting");
Run Code Online (Sandbox Code Playgroud)

服务A.cs

_logger.LogInformation("In Service A");
Run Code Online (Sandbox Code Playgroud)

然而,我遇到的问题是我需要 .ForContext 也能够在任何地方工作......并且它在我的主程序类中:

程序.cs

Log
   .ForContext("EventID", 42)
   .Information("Log This with EventID === 42");
Run Code Online (Sandbox Code Playgroud)

...但是,当我尝试在任一工作人员/服务类中执行相同操作时...

工人.cs

_logger
   .ForContext("EventID", 42)
   .Information("Log This with EventID === 42");
Run Code Online (Sandbox Code Playgroud)

...它不起作用,我收到以下错误:

错误CS1061

'ILogger<Worker>' does not contain a definition for 'ForContext' and no accessible extension method 'ForContext' accepting a first argument of type 'ILogger<Worker>' could be found
Run Code Online (Sandbox Code Playgroud)

...所以我对此进行了调查,并遇到了以下非常接近的问题(这两个问题我都无法应用,请参阅下面代码中的注释):

...(以及其他一些地方)但我无法将答案集成到代码库中:

程序.cs

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureAppConfiguration((context, config) =>
            {
                // Configure the app here.
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
                services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
                services.AddScoped<IServiceA, ServiceA>();
                services.AddScoped<IServiceB, ServiceB>();
                
                //?? I'm not sure if this .AddLogging(...) is needed ??
                services.AddLogging(x =>     
                {
                    x.ClearProviders();
                    x.AddSerilog(dispose: true);
                });
                //?? ... having/not having it doesn't seem to affect execution 
            })
            .UseSerilog();
Run Code Online (Sandbox Code Playgroud)

工人.cs

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    //private readonly Serilog.ILogger<Worker> _logger;
    //?? ... wrt this attempt, Serilog.ILogger won't allow <Worker> ...

    //??    private readonly ILogger _log = Log.ForContext<SomeService>();
    //private readonly ILogger _logger = Log.ForContext<Worker>();
    //?? ... wrt this attempt, Log isn't available ...
    
    private FileSystemWatcher _folderWatcher;
    private readonly string _inputFolder;
    private readonly IServiceProvider _services;

    public Worker(ILogger<Worker> logger, IOptions<AppSettings> settings, IServiceProvider services)
    {
        _logger = logger;
        _services = services;
        _inputFolder = settings.Value.InputFolder;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.CompletedTask;
    }

    public override Task StartAsync(CancellationToken cancellationToken)
    {
        _logger
            .ForContext("EventID", 1001)
            .LogInformation("Service Starting - need to assign EventID === 1001");
        //?? here is where .ForContext is needed (but doesn't work)
...    ...    ...
Run Code Online (Sandbox Code Playgroud)

Dai*_*Dai 8

前言:

  • 此答案使用“MEL”作为 的缩写Microsoft.Extensions.Logging。
  • 由于 MEL和Serilog 将类型名称ILogger用于完全不同的类型,因此此答案始终将名称出现的歧义消除为或。 ILoggerMEL.ILoggerSerilog.ILogger
    • 但请注意,只有 MELILogger<T>是通用的。没有Serilog.ILogger<T>,只有Serilog.ILogger。

TL;DR(对于.ForContext):

  • Serilog.ILogger.ForContext(String propertyName, Object? value, Boolean destructureObjects = false)创建一个“子”Serilog.ILogger实例,并将单个事件属性添加到子记录器记录的所有事件中。

    • 而在 MEL 中,您通常会在块中使用ILogger<T>'s并传递参数),它与 具有不同的语义。这就是为什么你不应该将 Serilog 惯用法与 MEL 惯用法混合。BeginScopeusing( log.BeginScope(...) )state: Dictionary<String,Object?>ForContext
  • Serilog.ILogger.ForContext(Type)和Serilog.ILogger.ForContext<TSource>都相当于调用ForContext(String).

    • (ForContext<TSource>()传递typeof(TSource)到ForContext(Type),并且ForContext(Type)刚刚传递Type.FullName到ForContext(String))。
  • MEL 相当于ForContext<TSource>...

    • ...通过注入 MELILogger<TSource>作为TSource构造函数参数来使用构造函数参数依赖注入。
    • 注入(或以其他方式获取)对 MEL 的引用ILoggerFactory并指定TSource为.CreateLogger<T>扩展方法的泛型类型参数。
    • 这两种方法都会使 MEL在注入期间自动为您ILoggerFactory添加一个"SourceContext"(或“类别”)事件属性。typeof(TSource).FullName
  • 因此,如果您将 MEL 注入ILogger<TService>到您的class TService构造函数中,那么......

    • ...您不需要调用ForContext自己来设置"SourceContext"(或“类别”)。
    • 当您想在服务中添加更多事件属性时,您应该使用 MEL,ILogger.BeginScope()而不是使用 MEL。Serilog.ILogger.ForContext
    • 如果您尝试使用ForContextMEL ILogger(通用版本和非通用版本),您将收到编译器错误,如果您尝试使用BeginScopewith Serilog.ILogger- 也是如此,因为这些是仅为其预期类型定义的扩展方法。

继续阅读...

有 2 种不同的方法可以在使用Microsoft.Extensions.DependencyInjection.

  1. 仅使用 Serilog 作为 MEL 的(特定于主机的)后端,同时使用 MEL 作为MEL.ILogger<T>注入类型构造函数的“前端”接口。
  2. 直接在您自己的代码中使用 Serilog(使用 的成员static,global::Serilog.Log或通过注入global::Serilog.ILogger),同时仍然为 Serilog 连接 MEL,以便使用 MEL 的其他组件(不是您编写的)仍将出现在 Serilog 输出中。

选项2是Serilog作者的偏好......

在应用程序代码中,我专门使用Serilog.ILogger和static global::Serilog.Log.*方法。

我也不喜欢让每个构造函数都带有MEL.ILogger<T>参数(构造函数参数列表是非常有价值的资产),并且发现一旦代码退出 DI 并开始调用想要进行一些日志记录的非 DI 实例化类,就会出现可用性问题(更多参数传递)。

...但是我(恭敬地)不同意@nblumhardt 的推理,因为:

  • 一般来说,使用任何不纯的(即非无副作用的)staticAPI(不仅仅是 Serilog 的Serilog.Log)都是一个坏主意,因为管理static单元和集成测试中的状态是非常困难的,如果不是不可能的话,这取决于您使用的 API以及您的测试运行程序如何工作。
    • 例如,如果您希望单元测试或集成测试对 SUT 记录的内容做出断言,那么您不能使用并发测试执行:它们必须严格按顺序执行,以便您的测试环境可以重置或重新配置Serilog.Log每个测试测试用例,否则所有测试输出将被混在一起。
  • 避免“可用性”以及“构造函数参数列表是非常有价值的不动产”的推理是在人体工程学中表达的,甚至只是美学,但这可能是不正确做某事的最糟糕的原因:“我要“因为美观而给自己带来工程问题”并不是向项目工程经理展示的好案例。
  • 虽然我意识到拥有 ctor params 会使在 DI 不可用的情况下重用类变得更加困难,但使用这些static Serilog.Log方法并没有更好:这意味着现在在静态 Serilog 类型不可用的情况下更难重用类。
    • MEL.ILogger<T>对于这种情况,我的首选解决方案是为该类型定义一个静态工厂方法,该方法提供(例如)的库存或 NOOP 实现NullLogger<T>,或者定义一个提供其自己的默认值的替代构造函数(并应用于[ActivatorUtilitiesConstructor]DI 构造函数)。
    • 此外,Microsoft.Extensions.Logging现在已被确立为基线日志记录库,现在每个 .NET 环境中都存在 - 考虑到.csproj过去 5 年中创建的任何随机项目,它更有可能MEL.ILogger<T>代替(如果不是补充的话)SerilogNuGet可用包裹。
      • 只有没有使用 的简单控制台项目和较旧的代码库IHost才没有 MEL 可用。此外,无论如何,每个ASP.NET Core 项目都有 MEL。

如果您使用不同的 DI 系统(例如 Simple Injector、Autofac、Ninject 等),情况会有所不同,因此,如果您不直接使用,请不要遵循本文的建议。Microsoft.Extensions.DependencyInjection


对于选项 1,我在项目中是这样做的:

  • 如果这是一个多项目解决方案,并且有一个.exe引用您其他项目的“入口点”项目......

    • 然后该.exe项目应引用Microsoft.Extensions.Logging、Serilog和Serilog.Extensions.LoggingNuGet 包。
    • 其他项目只需要引用Microsoft.Extensions.Logging.Abstractions(而不是主Microsoft.Extensions.Logging包)。
  • 如果这是单个项目解决方案,则参考Microsoft.Extensions.Logging、Serilog和Serilog.Extensions.Logging。

  • 如果您正在使用Host.CreateDefaultBuilder或 ,WebHost.CreateDefaultBuilder那么这些方法已经调用了.AddLogging您,您不需要自己执行此操作,但您确实需要在调用之前调用UseSerilog( Serilog.SerilogHostBuilderExtensions.UseSerilog) 。IHostBuilder.Build()

    • 您也不需要致电.AddSerilog。
  • 在您的服务类型(即在其构造函数参数中具有服务接口的类型)中,使用Microsoft.Extensions.Logging.ILogger<T>whereT与构造函数的声明类型相同的类型(是的,我同意这是多余的)。

    • 因此,除了您的文件(和/或您的方法所在的文件)之外,您不应该using Serilog在任何其他文件中包含该文件。.csProgram.csconfigureLogger
  • 然后,在运行时,当Microsoft.Extensions.DependencyInjection容器实例化您的类型时,它将Microsoft.Extensions.Logging.ILoggerFactory.ILoggerFactory.CreateLogger(String categoryName)自动调用您。

    • (其中是注入类型中categoryName的类型名称)TMEL.ILogger<T>
    • ...它被传递到 Serilog 的ForContext记录器工厂,返回的Serilog.ILogger结果由 MEL 的MEL.ILogger<T>.

一个实际的例子:

程序.cs

using System;
using Microsoft.Extensions.Logging;

// No `using Serilog` here.

static async Task<Int32> Main( String[] args )
{
    using( IHost host = CreateHost( args ) )
    {
        await host.RunAsync();

        return 0;
    }
}

static IHost CreateHost( String[] args )
{
    IHostBuilder b = Host.CreateDefaultBuilder( args );
    // CreateDefaultBuilder calls `MEL.AddLogging` for you already.
    b = b.UseStartup<YourStartupClass>(); // or `b.ConfigureServices()`;

    // Explicit extension method call because there's no `using Serilog` to avoid ambiguity issues:
    b = global::Serilog.SerilogHostBuilderExtensions.UseSerilog(
        builder             : b,
        configureLogger     : ConfigureSerilog,
        preserveStaticLogger: true
    );

    return b.Build();
}

static void ConfigureSerilog( HostBuilderContext ctx, Serilog.LoggerConfiguration logCfg )
{
     _ = logCfx
        .Enrich.WithExceptionDetails()
        .Enrich.FromLogContext()
        .MinimumLevel.Is( /* etc */ )
        .WriteTo.File( /* etc */ );
}
Run Code Online (Sandbox Code Playgroud)

示例服务类型.cs

using System;
using Microsoft.Extensions.Logging;

// No `using Serilog` here either.

public MyService
{
    private readonly ILogger log;

    public MyService( ILogger<MyService> log )
    {
        this.log = log ?? throw new ArgumentNullException(nameof(log));
    }

    public void Foo( String name )
    {
        this.log.LogInformation( "hello {Name}", name );
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您发现自己MyService在 DI 不可用的地方重用,那么您可以定义一个替代构造函数,如下所示:

using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.DependencyInjection;

// No `using Serilog` here either.

public MyService
{
    private readonly ILogger log;

    [ActivatorUtilitiesConstructor]
    public MyService( ILogger<MyService> log )
    {
        this.log = log ?? throw new ArgumentNullException(nameof(log));
    }

    public MyService()
        : this( log: NullLogger<MyService>.Instance )
    {
    }

    public void Foo( String name )
    {
        this.log.LogInformation( "hello {Name}", name );
    }
}
Run Code Online (Sandbox Code Playgroud)

所以这将起作用(尽管在NullLogger<T>专门使用时,不会记录任何内容,因此可能或可能不理想):

void Bar()
{
    MyService svc = new MyService();
    svc.Foo();
}
Run Code Online (Sandbox Code Playgroud)

  • +1很好的完整答案,但必须对稻草人提出问题:_然后你不能对你的类进行单元测试或集成测试,你也不能切实地模拟/存根/伪造你正在使用的静态依赖项。_/尼克的观点是如果您没有显式测试日志记录,请静态进行。如果您需要在测试上下文中控制它,没有什么可以阻止您*根据需要显式地显示依赖项(并在产品组合根中使用“Log.Logger”来实现它),*但这是一件非常罕见的事情*。 (2认同)