9

I follow this article. Developing a sink

But I need to injection IHttpClientFactory.

public class MySink : ILogEventSink
{
    //...

    public MySink(IFormatProvider formatProvider, IHttpClientFactory httpClientFactory)
    {
        //...
    }
}

public static class MySinkExtensions
{
    public static LoggerConfiguration MySink(
        this LoggerSinkConfiguration loggerConfiguration, 
        IHttpClientFactory httpClientFactory, 
        IFormatProvider formatProvider = null,
        LogEventLevel restrictedToMinimumLevel = LogEventLevel.Verbose)
    {
        return loggerConfiguration.Sink(new MySink(formatProvider, httpClientFactory), restrictedToMinimumLevel);
    }
}

Program Main:

class Program
{
    static async Task Main(string[] args)
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddHttpClient();

        var logger = new LoggerConfiguration()
            .ReadFrom.Configuration(configuration)
            .WriteTo.MySink(null) //<--How to get IHttpClientFactory?
            .CreateLogger();

        services.AddLogging(configure => configure.ClearProviders().AddSerilog(logger));

        var serviceProvider = services.BuildServiceProvider();
    }
}

appsettings:

{
"Serilog": {
    "Using": [ "Serilog.Sinks.Console", "MyTestProject" ],
    "WriteTo": [
        {
            "Name": "Console",
            "Args": {
                "restrictedToMinimumLevel": "Debug"
            }
        },
        {
            "Name": "MySink",
            "Args": {
                "restrictedToMinimumLevel": "Warning"
            }
        }
    ]
}

Can I use the Args to injection IHttpClientFactory?

Or handle this in Main method?

Note: I am using net core console application.

Update: According to the asnwer

Now, the Program Main is:

class Program
    {
        static async Task Main(string[] args)
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddHttpClient();

            serviceCollection.AddSingleton<Serilog.ILogger>(sp =>
            {
                var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();

                return new LoggerConfiguration()
                    .MinimumLevel.Debug()
                    .WriteTo.MySink(httpClientFactory)
                    .CreateLogger();
            });

            serviceCollection.AddLogging(configure => configure.ClearProviders().AddSerilog());

            var serviceProvider = serviceCollection.BuildServiceProvider();

            Log.Logger = serviceProvider.GetRequiredService<Serilog.ILogger>();
        }
    }

And I can injection ILogger in other class

class Test
{
    private readonly ILogger<Test> _logger;

    Test(ILogger<Test> logger)
    {
        _logger = logger;
    }
}

Update: (2020-07-01)

Now, in asp net core, this package supply dependency injection.

.UseSerilog((hostingContext, services, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration)
.Enrich.FromLogContext()
.WriteTo.SomeSink(services.GetRequiredService<ISomeDependency>()));
6
  • Why do you need to inject IHttpClientFactory? For a sink you would just pass in configuration options e.g. a URL etc. as the sink would know what it needs to create i.e. an HttpClient, to function. If you look at the code for the Elastic Search Sink you can see they pass in URI's but the sink creates the client itself - ElasticLowLevelClient Commented Aug 8, 2019 at 4:32
  • 1
    Because I found some problem with HttpClient.Issues with the original HttpClient class available in .NET Core. Commented Aug 8, 2019 at 5:15
  • In that case can you just use new HttpClientFactory() in the sink instead and call that to create your client? Commented Aug 8, 2019 at 6:29
  • 1
    I can't new HttpClientFactory. IHttpClientFactory is implemented by DefaultHttpClientFactory(DefaultHttpClientFactory), and it is a internal class. Commented Aug 8, 2019 at 7:50
  • @Libron Have you gotten the concept to work that you added in your update 2020-07-01? Commented Dec 22, 2022 at 10:17

3 Answers 3

7

I have played around a bit and found you can use an implementation factory to get the IHttpClientFactory after the service provider has been built. You need to change the service.AddLogging(...) call to use this factory implementation:

static async Task Main(string[] args)
{
    var serviceCollection = new ServiceCollection();
    serviceCollection.AddHttpClient();

    serviceCollection.AddSingleton<Serilog.ILogger>(sp =>
    {
        var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();

        return new LoggerConfiguration()
            .MinimumLevel.Debug()
            //.ReadFrom.Configuration(Configuration)
            .WriteTo.MySink(httpClientFactory)
            .CreateLogger();
    });

    serviceCollection.AddLogging(cfg =>
    {
        cfg.ClearProviders().AddSerilog();
    });


    var serviceProvider = serviceCollection.BuildServiceProvider();


    var logger = serviceProvider.GetService<Serilog.ILogger>();

    logger.Debug("Working");
}

In the code sample we can use a factory implementation for AddSingleton to only create the logger when it is first requested. This allows us to pull any registered services from the service provider and inject them into our class. In this case, we get the IHttpClientFactory and inject it into MySink.

If you put a breakpoint on the line var logger = serviceProvider.GetService<Serilog.ILogger>() when you step into that it will call the lambda in AddSingleton and create your logger for you.

Sign up to request clarification or add additional context in comments.

Comments

1

I analysed the problems I had here, and the solution by Simply Ged almost works. The problem with it is, that whereas Serilog.ILogger now correctly resolves from DI, this instance has not been passed to AddSerilog(). This means that if you inject an ILogger<> somewhere else in your app, it won't use to the Serilog.ILogger registered in DI as underlying logger, but the static Serilog.Log.

I arrived at my solution by inspecting the code of AddSerilog and found that in some code paths the passed logger is registered in DI from a factory method. However, since the logger must be passed to the method, the service provider from the factory method is not useful and is discarded.

This prompted me to design an alternative AddSerilog extension method:

    /// <summary>
    /// Add Serilog to the logging pipeline, this extension method injects the service provider into the configure method.
    /// This allows you to resolve injected services while building the logger.
    /// Take care that you do not create circular dependency issues!
    /// </summary>
    /// <param name="builder">The <see cref="Microsoft.Extensions.Logging.ILoggingBuilder" /> to add logging provider to.</param>
    /// <returns>Reference to the supplied <paramref name="builder" />.</returns>
    public static ILoggingBuilder AddSerilog(
        this ILoggingBuilder builder,
        Func<IServiceProvider, LoggerConfiguration, LoggerConfiguration> configure
    )
    {
        ArgumentNullException.ThrowIfNull(builder);
        builder.Services.AddSingleton<ILoggerProvider, SerilogLoggerProvider>(
            (IServiceProvider sp) =>
                new SerilogLoggerProvider(configure(sp, new LoggerConfiguration()).CreateLogger(), dispose: true)
        );
        builder.AddFilter<SerilogLoggerProvider>(null, LogLevel.Trace);
        return builder;
    }

When using this method, you register it like this:

new ServiceCollection()
    .AddHttpClient()
    .AddLogging(loggingBuilder =>
        loggingBuilder.AddSerilog(
            (sp, config) =>
                config
                    .MinimumLevel.Debug()
                    .ReadFrom.Configuration(Configuration)
                    .WriteTo.MySink(sp.GetRequiredService<IHttpClientFactory>())
        )
    )
    .BuildServiceProvider()
    .GetRequiredService<ILogger>()
    .Debug("Working");

As noted in the extension method take care to avoid a circular dependency. If you in any way try to resolve a service that injects ILogger<> in the factory method, there's a good chance you get stuck in a loop. I for example had a sink that was registered in DI, so it could inject multiple dependencies. I had to take care to lazily resolve those services to avoid a loop. For lazy resolution we use Thomas Levesque's approach.

Comments

0

The issue with using IHttpClientFactory and Serilog Custom Sink is that once the dependency is resolved, DefaultHttpClientFactory (which implements IHttpClientFactory) depends on ILogger. This creates a circular loop if the ILogger is injected directly.

To resolve this issue, use a Lazy implementation to avoid the loop. The following code demonstrates how to do this in a custom sink extension class:

public static class MySinkExtensions
{
    public static LoggerConfiguration MySink(
        this LoggerSinkConfiguration loggerConfiguration, 
        Lazy<IHttpClientFactory> lazyHttpClientFactory, 
        IFormatProvider formatProvider = null,
        LogEventLevel restrictedToMinimumLevel = LogEventLevel.Verbose)
    {
        return loggerConfiguration.Sink(new MySink(formatProvider, 
        lazyHttpClientFactory), restrictedToMinimumLevel);
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.