.NET Core - How to filter based on category?

Viewed 3902

So I've got a basic custom logger, and set up as so:

public static IWebHost CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseIISIntegration()
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddConsole();
            logging.AddCustomLogger().AddFilter<CustomLoggerProvider>(typeof(CustomLogEntry).ToString(), LogLevel.None); // My custom logger
            logging.SetMinimumLevel(LogLevel.Trace);
        }).Build();
}

My understanding is that the .AddFilter means that only loggers of

typeof(CustomLogEntry) 

should actually log out to my custom logger. That doesn't appear to be the case, though!

The extension to add the actual logger is pretty straightforward:

public static ILoggingBuilder AddCustomLogger(this ILoggingBuilder builder)
    {
        builder.AddConfiguration();
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, CustomLoggerProvider>());
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CustomLoggerOptions>, CustomLoggerOptionsSetup>());
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<LoggerOptions>, CustomLoggerProviderOptionsChangeTokenSource<CustomLoggerOptions, CustomLoggerProvider>>());
        return builder;
    }

My custom logger is pretty straightforward as well:

public class CustomLogger : ILogger
{
    public CustomLoggerProvider Provider { get; private set; }
    public string Category { get; private set; }

    public Logger(CustomLoggerProvider provider, string category)
    {
        Provider = provider;
        Category = category;
    }

    public IDisposable BeginScope<T>(T item)
    {
        return Provider.ScopeProvider.Push(item);
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return Provider.IsEnabled(logLevel);
    }

    public void Log<T>(LogLevel logLevel, EventId eventId, T item, Exception exception, Func<T, Exception, string> formatter)
    {
        if (IsEnabled(logLevel))
        {
            // Generate the log entry
            CustomLogEntry logEntry = new CustomLogEntry();

            // removed for brevity...

            // Write the log out to the provider!
            Provider.WriteLog(logEntry);
        }
    }
}

And the provider:

[ProviderAlias("Custom")]
public class CustomLoggerProvider : ILoggerProvider, IDisposable, ISupportExternalScope
{
    private ConcurrentQueue<CustomLogEntry> _logQueue = new ConcurrentQueue<CustomLogEntry>();
    private readonly IDisposable _settingsChangeToken;
    private readonly CustomLogger _customLogger;
    private CustomLoggerOptions _settings;
    private bool _terminated = false;

    public IExternalScopeProvider ScopeProvider { get; set; }

    public CustomLoggerProvider(IOptionsMonitor<LoggerOptions> settings, IConfiguration configuration) : this(settings.CurrentValue, configuration)
    {
        // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/change-tokens
        _settingsChangeToken = settings.OnChange(options =>
        {
            _settings = options;
        });
    }

    public CustomLoggerProvider(CustomLoggerOptions settings, IConfiguration configuration)
    {
        _settings = settings;
        _customLogger = new CustomLogger(this, typeof(CustomLogEntry).ToString());

        ProcessQueue();
    }

    public ILogger CreateLogger(string categoryName)
    {
        return _customLogger;
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return logLevel == LogLevel.Information;
    }

    public void WriteLog(LogEntry logEntry)
    {
        _logQueue.Enqueue(logEntry);
    }
    private void ProcessQueue()
    {
        Task.Run(async () =>
        {
            while (!_terminated)
            {
                try
                {
                    // chew up the queue
                    if (_logQueue.TryDequeue(out CustomLogEntry logEntry))
                    {
                        // Do work, removed for brevity
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        });
    }

    #region IDisposable Support
    private bool disposedValue = false; // To detect redundant calls

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects).
                _terminated = true;
            }

            // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
            // TODO: set large fields to null.

            disposedValue = true;
        }
    }

    // This code added to correctly implement the disposable pattern.
    void IDisposable.Dispose()
    {
        // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        Dispose(true);
    }
    #endregion

    #region ISupportExternalScope Support
    public void SetScopeProvider(IExternalScopeProvider scopeProvider)
    {
        ScopeProvider = scopeProvider;
    }
    #endregion
}

So after putting this together and testing it out, I'm seeing every log of LogLevel.Trace or higher being logged to the CustomLogger.

What I'm trying to do, is have only logs of a specific category actually log out to the custom provider. Or in other words, filter which log provider to log out to, based on the category.

Is what I'm doing not possible? Am I wildly misunderstanding how .AddFilter actually works? (probably, lol)

I'm suspecting that my CustomLoggerProvider.IsEnabled somehow needs to check that the incoming log category is correct, but I'm just not seeing how to make that happen.

I'd appreciate any pointers.

2 Answers

The first param to AddFilter is a namespace to apply it to. Currently, you're passing nameof(CustomLogEntry), which isn't going to work. That would essentially be saying only apply this to messages logged from CustomLogEntry (i.e. internally in the class). Though, even that wouldn't work, because it wouldn't have the full namespace to CustomLogEntry. What you need to do is something like:

.AddFilter<CustomLoggerProvider>("Microsoft", LogLevel.None)

That would then apply the filter to any logs originating anywhere in a namespace beginning with Microsoft, i.e. Microsoft.AspNetCore, Microsoft.Extensions.Configuration, etc. Obviously, you'd add more of the namespace to limit it further.

So here's what I ended up doing:

public static IWebHost CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
    .UseIISIntegration()
    .ConfigureLogging((hostingContext, logging) =>
    {
        logging.AddConsole();
        logging.AddCustomLogger().AddFilter<CustomLoggerProvider>((category, logLevel) => {
                if (category.ToUpper().Contains("Custom.Namespace") && logLevel != LogLevel.None)
                {
                    return true;
                } 
                else
                {
                    return false;
                }
            }); // My custom logger
        logging.SetMinimumLevel(LogLevel.Trace);
    }).Build();

That is based off the information I found here: MSDN AddFilter Extensions, specifically this signature: AddFilter(ILoggingBuilder, Func<String,LogLevel,Boolean>)

That correctly did what I sought to do - any category that corresponded to my custom namespace now returns True, which ran the correct CustomLoggerProvider I wanted executed.

Related