带有注入的自定义 Serilog 接收器?

Custom Serilog sink with injection?

我创建了一个简单的 Serilog 接收器项目,如下所示:

namespace MyApp.Cloud.Serilog.MQSink
{
    public class MessageQueueSink: ILogEventSink
    {
        private readonly IMQProducer _MQProducerService;
        public MessageQueueSink(IMQProducer mQProducerService)
        {
            _MQProducerService = mQProducerService;
        }
        public void Emit(LogEvent logEvent)
        {
            _MQProducerService.Produce<SendLog>(new SendLog() { LogEventJson = JsonConvert.SerializeObject(logEvent)});
        }
    }
}

消费微服务是这样启动的:

        var configurationBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
        var appSettings = configurationBuilder.Get<AppSettings>();

        configurationBuilder = new ConfigurationBuilder().AddJsonFile("ExtendedSettings.json").Build();

            Host.CreateDefaultBuilder(args)
                .UseMyAppCloudMQ(context => context.UseSettings(appSettings.MQSettings))
                .UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration))
                .ConfigureServices((hostContext, services) =>
                {
                    services
                        .AddHostedService<ExtendedProgService>()
                        .Configure<MQSettings>(configurationBuilder.GetSection("MQSettings"))
                })
                .Build().Run();

appsettings.json 的 serilog 部分如下所示:

  "serilog": {
    "Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Console", "MyApp.Cloud.Serilog.MQSink" ],
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId" ],
    "WriteTo": [
      {
        "Name": "MessageQueueSink",
        "Args": {}
        }
    ]
  }

添加 MQSink 项目作为对微服务项目的引用,我可以看到 MQSink dll 最终位于 bin 文件夹中。

问题是,当在微服务中执行 _logger.LogInformation(...) 时,Emit 永远不会被触发,但是如果我添加一个控制台接收器,它会输出数据吗?我也怀疑注入的MQ会不会正常工作?

如何解决?

编辑:

打开Serilog内部日志,发现找不到MessageQueueSink方法。我没有找到任何方法使它与 appsetings.json 一起工作,所以我开始研究如何在代码中绑定它。

要使其正常工作,必须创建一个扩展程序:

public static class MySinkExtensions
    {
        public static LoggerConfiguration MessageQueueSink(
                  this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
                  MyApp.Cloud.MQ.Interface.IMQProducer mQProducer = null)
        {
            return loggerConfiguration.Sink(new MyApp.Cloud.Serilog.MQSink.MessageQueueSink(mQProducer));
        }
    }

这使得添加自定义接收器成为可能:

Host.CreateDefaultBuilder(args)
                    .UseMyAppCloudMQ(context => context.UseSettings(appSettings.MQSettings))
                     .ConfigureServices((hostContext, services) =>
                    {
                        services
                            .Configure<MQSettings>(configurationBuilder.GetSection("MQSettings"))
                    })
                    .UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration).WriteTo.MessageQueueSink())
                    .Build().Run();

自定义sink已经加载,Emit也被触发了,但是我仍然不知道如何将MQ注入到sink中?如果我可以在 appsettings.json 文件中完成 Serilog 和 sink 的所有配置,那也会好得多。

如果您参考 Provided Sinks 列表并检查其中一些的源代码,您会发现模式通常是:

  1. 构造接收器配置(通常从 IConfiguration、内联或两者的组合中获取值)
  2. 将配置传递给接收器注册。

然后接收器实现实例化所需的服务以将日志推送到。

我建议的另一种方法是不带任何参数注册 Serilog (UseSerilog()),然后使用内置的 IServiceProvider 配置静态 Serilog.Log class:

var host = Host.CreateDefaultBuilder(args)
    // Register your services as usual
    .UseSerilog()
    .Build()
    
Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(host.Services.GetRequiredService<IConfiguration>())
    .WriteTo.MessageQueueSink(host.Services.GetRequiredService<IMQProducer>())
    .CreateLogger();

host.Run();

为了让依赖注入流程处理所有这些,您可以使用 ReadFrom.Services 扩展方法。
此扩展方法是 Serilog.Extensions.Hosting NuGet package, which is also bundled with the Serilog.AspNetCore NuGet 包的一部分。

来自方法描述

Configure the logger using components from the services.
If present, the logger will receive implementations/instances of Serilog.Core.LoggingLevelSwitch, Serilog.Core.IDestructuringPolicy, Serilog.Core.ILogEventFilter, Serilog.Core.ILogEventEnricher, Serilog.Core.ILogEventSink, and Serilog.Configuration.ILoggerSettings.

因此,如果您将 MessageQueueSink 注册到 DI 容器中,它将被注入 Serilogs Logger 并且轮到它也将注入自己的依赖项(此处:一个 IMQProducer 实例)。
对于静态 Serilog.Log.Logger 以及在某处注入的任何 ILogger<T> 实例都会发生这种情况。

请注意,此设置不是 require/use LoggerSinkConfiguration 上的扩展方法。

在下面找到代码部分。


DI 容器和 Serilog 的设置

class Program
{
    static void Main(string[] args)
    {
        using var host = CreateHostBuilder(args);
        host.Services.GetService<IMyService>().Run();
        host.Run();
    }

    public static IHost CreateHostBuilder(string[] args)
        => Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostBuilderContext, services) => 
                {
                    services.AddTransient<ILogEventSink, MessageQueueSink>();
                    services.AddTransient<IMQProducer, MQProducer>();
                    // ...
                    services.AddTransient<IMyService, MyService>();
                })
                .UseSerilog((context, services, configuration) 
                    => configuration
                        .ReadFrom.Services(services)
                        .ReadFrom.Configuration(context.Configuration)
                )
                .Build();
}

为了完整起见,上面提到的另一个类:

水槽

public class MessageQueueSink : ILogEventSink
{
    private readonly IMQProducer _mqProducer;

    public MessageQueueSink(IMQProducer mqProducer)
        => _mqProducer = mqProducer;
    
    public void Emit(LogEvent logEvent)
        => _mqProducer.Produce(logEvent);
}

上面提到的其他services/dependencies

// Stub for the real MQ stuff.
public interface IMQProducer
{
    void Produce(LogEvent logEvent);
}

public class MQProducer : IMQProducer
{
    public void Produce(LogEvent logEvent)
        => Console.WriteLine($">>> Producing log event: {logEvent.RenderMessage()}");
}   

public interface IMyService
{
    void Run();
}

public class MyService : IMyService
{
    private readonly Microsoft.Extensions.Logging.ILogger _logger;

    public MyService(ILogger<MyService> logger)
        => _logger = logger;

    public void Run()
    {
        _logger.LogError("From injected logger");
        Serilog.Log.Logger.Error("From static logger");
    }
}

只要你的接收器的依赖有一个默认的构造函数,它们将被实例化,一个完整的配置方法是可能的。

在下面 appsettings.json 的片段中,WriteTo 部分提到了 Serilog 激活扩展方法的名称 MessageQueueSink - 更多信息 -
以及必须实例化并为该扩展方法的参数 mqProducer 传递的对象的 type

(我的程序集名为 MyApplication 并且有一个 MyApplication.System 命名空间。)

"Serilog": {
    "Using": [ "MyApplication" ],
    "MinimumLevel": {
        "Default": "Information"
    },
    "WriteTo": [
        {
            "Name": "MessageQueueSink",
            "Args": {
                "mqProducer": "MyApplication.System.MQProducer, MyApplication"
            }
        }
    ]
}

调用 ReadFrom.Configuration 就足够了。

namespace MyApplication.System          
{           
    class Program
    {
        static void Main(string[] args)
        {
            using var host = CreateHostBuilder(args);
            host.Services.GetService<IMyService>().Run();
            host.Run();
        }

        public static IHost CreateHostBuilder(string[] args)
            => Host.CreateDefaultBuilder(args)
                    .ConfigureServices((hostBuilderContext, services)
                        => services.AddTransient<IMyService, MyService>()
                    )
                    .UseSerilog((context, services, configuration) 
                        => configuration
                                .ReadFrom.Configuration(context.Configuration)
                    )
                    .Build();
        }
    }
}

此方法需要基于 Serilog.Configuration.LoggerSinkConfiguration 的扩展方法,因为这是 json 设置中提到的方法。

由于上面的json设置已经指定了"Args": { "mqProducer": "MyApplication.System.MQProducer, MyApplication" },所以会自动传入一个该类型的实例。

public static class LoggerSinkConfigurationExtensions
{
    public static LoggerConfiguration MessageQueueSink(
        this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
        IMQProducer mqProducer = null
        ) =>  loggerConfiguration.Sink(new MessageQueueSink(mqProducer));        
}

如果无法通过默认构造函数实例化该类型,您可能需要自己进行实例化;
例如loggerConfiguration.Sink(new MessageQueueSink(new MQProducer( /* arguments */ )))


水槽

public class MessageQueueSink : ILogEventSink
{
    private readonly IMQProducer _mqProducer;

    public MessageQueueSink(IMQProducer mqProducer)
        => _mqProducer = mqProducer;
    
    public void Emit(LogEvent logEvent)
        => _mqProducer.Produce(logEvent);
}

上面提到的其他services/dependencies

public interface IMQProducer
{
    void Produce(LogEvent logEvent);
}

public class MQProducer : IMQProducer
{
    public void Produce(LogEvent logEvent)
        => Console.WriteLine($">>> Producing log event: {logEvent.RenderMessage()}");
}

public interface IMyService
{
    void Run();
}

public class MyService : IMyService
{
    private readonly Microsoft.Extensions.Logging.ILogger _logger;

    public MyService(ILogger<MyService> logger)
        => _logger = logger;

    public void Run()
    {
        _logger.LogError("From injected logger");
        Serilog.Log.Logger.Error("From static logger");
    }
}

@pfx 帮助如何加载 Serilog 自定义接收器,为此我给了他赏金,谢谢!然而,这不是我的最终解决方案。

依赖注入意味着 class 的构造函数将必要的对象作为参数。虽然无法使用构造函数参数加载自定义接收器,但必须通过服务来完成。

public static IHost CreateHostBuilder(string[] args)
           => Host.CreateDefaultBuilder(args)
                    .ConfigureServices((hostContext, services) =>
                    {
                        services
                            .AddTransient<IMyService, MyService>()
                            .AddTransient<ICommunicator, Communicator>()
                            .AddTransient<ILogEventSink, CustomSerilogSink>();
                    })
                   .UseSerilog((context, services, configuration) => configuration
                                                                       .ReadFrom.Configuration(context.Configuration)
                                                                       .ReadFrom.Services(services)
                                                                       .WriteTo.Console(LogEventLevel.Information)
                                                                       .Enrich.FromLogContext())
           .Build();
        }

问题是 MQ 控制器 class 注入了一个 ILogger,这创建了一个导致释放的循环引用。 MQ 控制器是共享的,所以我不能对其进行太多更改,但我可以将所需的方法设为静态。他们没有使用本地 MQ 对象,而是被发送到方法中。

但是我无法删除 MS ILogger 要求,我仍然需要能够在自定义接收器中登录。所以首先我创建了一个这样的 Serilog:

private static readonly ILogger _logger = Log.ForContext<MessageQueueSink>() as ILogger;

然后可以将此记录器包装在 MS Ilogger class 中并发送到静态生成器方法:

public class CustomSerilogger : Microsoft.Extensions.Logging.ILogger
{
    private readonly ILogger _logger;
    public CustomSerilogger(ILogger logger)
    { _logger = logger; }

    public IDisposable BeginScope<TState>(TState state) => default!;

    public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { return _logger.IsEnabled(LogLevelToLogEventLevel(logLevel)); }

    public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
            return;

        _logger.Write(LogLevelToLogEventLevel(logLevel), exception, state.ToString());
    }

    private LogEventLevel LogLevelToLogEventLevel(Microsoft.Extensions.Logging.LogLevel loglevel)
    {
        switch(loglevel)
        {
            case Microsoft.Extensions.Logging.LogLevel.Debug:
                return LogEventLevel.Debug;
            case Microsoft.Extensions.Logging.LogLevel.Information:
                return LogEventLevel.Information;
            case Microsoft.Extensions.Logging.LogLevel.Warning:
                return LogEventLevel.Warning;
            case Microsoft.Extensions.Logging.LogLevel.Error:
                return LogEventLevel.Error;
            case Microsoft.Extensions.Logging.LogLevel.Critical:
                return LogEventLevel.Fatal;
            case Microsoft.Extensions.Logging.LogLevel.None:
                return LogEventLevel.Verbose;
            case Microsoft.Extensions.Logging.LogLevel.Trace:
                return LogEventLevel.Verbose;
        }
        return LogEventLevel.Verbose;
    }
}

要配置接收器,我必须在 appsettings.json 文件中创建自定义接收器设置部分,然后将其读入设置对象。

var mqSinkSettings = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build()
                                                    .GetSection("MessageQueueSinkSettings").Get<MessageQueueSinkSettings>();

终于成功了!