Search code examples
c#asp.net-core-middlewareilogger

C# How to use ISupportExternalScope with customer ILogger/ILoggerProvider and middleware


I have a custom ILogger / ILoggerProvider implementation that is basically a fancy JsonConsole logger to format things as required for our console log forwarding.

Everything works fine, including using ILogger.BeginScope to pass custom data to the log method(s).

Next, I am trying to write some middleware that looks at the HttpContext and automagically adds certain fields to the log scope.

What I have so far - I've trimmed out everthing not related to the log scope:


public class MyLogger : ILogger
{
    // ...

    private readonly string _categoryName;
    private IExternalScopeProvider _scopeProvider { get; set; }

    public MyLogger(string categoryName, IExternalScopeProvider scopeProvider)
    {
        _categoryName = categoryName;
        _scopeProvider = scopeProvider;
    }

    // ...
}

public class MyLoggerProvider : ILoggerProvider
{
    // ...

    public ILogger CreateLogger(string categoryName)
    {
        return _loggers.GetOrAdd(categoryName, new MyLogger(
            categoryName,
            new LoggerExternalScopeProvider()
        ));
    }
    // ...
}

public static ILoggingBuilder AddMyLogger(this ILoggingBuilder builder) 
{
    builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, MyLoggerProvider>());
    return builder;
}

// Program.cs

services.AddLogging(logBuilder => 
{
    logBuilder.ClearProviders();
    logBuilder.AddMyLogger();
});


// !!! Everything below here is conjecture - I don't have it working yet.

public class MyAutoLogScopeMiddleware
{
    private readonly RequestDelegate _next;

    public MyAutoLogScopeMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext, IExternalScopeProvider scopeProvider)
    {
        var logInfo = "Hi there!";
        
        using (scopeProvider.Push(logInfo))
        {
            await _next(httpContext);
        }
    }
}

public static class MyAutoLogScopeMiddlewareExtensions
{
    public static IApplicationBuilder UseMyAutoLogScope(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyAutoLogScopeMiddleware>();
    }
}

// Program.cs

app.UseMyAutoLogScope();

My question is how do I get the IExternalScopeProvider into the provider via dependency injection instead of newing one up?

Should I be registering it as .AddScoped<IExternalScopeProvider,LoggerExternalScopeProvider>(), and then change the provider to .AddScoped as well?

I feel like I should be using ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider) on MyLoggerProvider, but I can't find any examples of how to use that with dependency injection.

If anyone has a working example, I'd really appreciate a link to a blog post or article.

Thanks.


Solution

  • Your logger provider should implement ISupportExternalScope and pass along any scope provider to its loggers.

    public class MyLoggerProvider : ILoggerProvider, ISupportExternalScope
    {
        private IExternalScopeProvider _externalScopeProvider;
    
        void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider externalScopeProvider) =>
            _externalScopeProvider = externalScopeProvider
    
        public ILogger CreateLogger(string categoryName)
        {
            return _loggers.GetOrAdd(categoryName, new MyLogger(
                categoryName,
                _externalScopeProvider
            ));
        }
    }
    

    The logger factory system will call ISupportExternalScope.SetScopeProvider. You don't need to do it, and you don't need to provide an ISupportExternalScope to the DI, either.

    Your application code should not call IExternalScopeProvider.Push to create scopes; instead, use ILogger.BeginScope:

    public class MyAutoLogScopeMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger<MyAutoLogScopeMiddleware> _logger;
    
        public MyAutoLogScopeMiddleware(RequestDelegate next, ILogger<MyAutoLogScopeMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }
    
        public async Task InvokeAsync(HttpContext httpContext)
        {
            var logInfo = "Hi there!";
            
            using (_logger.BeginScope(logInfo))
            {
                await _next(httpContext);
            }
        }
    }