Implement a custom logging provider in .NET

There are many logging providers available for common logging needs. You may need to implement a custom ILoggerProvider when one of the available providers doesn't suit your application needs. In this article, you'll learn how to implement a custom logging provider that can be used to colorize logs in the console.

Tip

The custom logging provider example source code is available in the Docs Github repo. For more information, see GitHub: .NET Docs - Console Custom Logging.

Sample custom logger configuration

The sample creates different color console entries per log level and event ID using the following configuration type:

using Microsoft.Extensions.Logging;

public sealed class ColorConsoleLoggerConfiguration
{
    public int EventId { get; set; }

    public Dictionary<LogLevel, ConsoleColor> LogLevelToColorMap { get; set; } = new()
    {
        [LogLevel.Information] = ConsoleColor.Green
    };
}

The preceding code sets the default level to Information, the color to Green, and the EventId is implicitly 0.

Create the custom logger

The ILogger implementation category name is typically the logging source. For example, the type where the logger is created:

using Microsoft.Extensions.Logging;

public sealed class ColorConsoleLogger(
    string name,
    Func<ColorConsoleLoggerConfiguration> getCurrentConfig) : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;

    public bool IsEnabled(LogLevel logLevel) =>
        getCurrentConfig().LogLevelToColorMap.ContainsKey(logLevel);

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

        ColorConsoleLoggerConfiguration config = getCurrentConfig();
        if (config.EventId == 0 || config.EventId == eventId.Id)
        {
            ConsoleColor originalColor = Console.ForegroundColor;

            Console.ForegroundColor = config.LogLevelToColorMap[logLevel];
            Console.WriteLine($"[{eventId.Id,2}: {logLevel,-12}]");
            
            Console.ForegroundColor = originalColor;
            Console.Write($"     {name} - ");

            Console.ForegroundColor = config.LogLevelToColorMap[logLevel];
            Console.Write($"{formatter(state, exception)}");
            
            Console.ForegroundColor = originalColor;
            Console.WriteLine();
        }
    }
}

The preceding code:

  • Creates a logger instance per category name.
  • Checks _getCurrentConfig().LogLevelToColorMap.ContainsKey(logLevel) in IsEnabled, so each logLevel has a unique logger. In this implementation, each log level requires an explicit configuration entry to log.

It's a good practice to call ILogger.IsEnabled within ILogger.Log implementations since Log can be called by any consumer, and there are no guarantees that it was previously checked. The IsEnabled method should be very fast in most implementations.

TState state,
Exception? exception,

The logger is instantiated with the name and a Func<ColorConsoleLoggerConfiguration>, which returns the current config—this handles updates to the config values as monitored through the IOptionsMonitor<TOptions>.OnChange callback.

Important

The ILogger.Log implementation checks if the config.EventId value is set. When config.EventId is not set or when it matches the exact logEntry.EventId, the logger logs in color.

Custom logger provider

The ILoggerProvider object is responsible for creating logger instances. It's not necessary to create a logger instance per category, but it makes sense for some loggers, like NLog or log4net. This strategy allows you to choose different logging output targets per category, as in the following example:

using System.Collections.Concurrent;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

[UnsupportedOSPlatform("browser")]
[ProviderAlias("ColorConsole")]
public sealed class ColorConsoleLoggerProvider : ILoggerProvider
{
    private readonly IDisposable? _onChangeToken;
    private ColorConsoleLoggerConfiguration _currentConfig;
    private readonly ConcurrentDictionary<string, ColorConsoleLogger> _loggers =
        new(StringComparer.OrdinalIgnoreCase);

    public ColorConsoleLoggerProvider(
        IOptionsMonitor<ColorConsoleLoggerConfiguration> config)
    {
        _currentConfig = config.CurrentValue;
        _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig);
    }

    public ILogger CreateLogger(string categoryName) =>
        _loggers.GetOrAdd(categoryName, name => new ColorConsoleLogger(name, GetCurrentConfig));

    private ColorConsoleLoggerConfiguration GetCurrentConfig() => _currentConfig;

    public void Dispose()
    {
        _loggers.Clear();
        _onChangeToken?.Dispose();
    }
}

In the preceding code, CreateLogger creates a single instance of the ColorConsoleLogger per category name and stores it in the ConcurrentDictionary<TKey,TValue>. Additionally, the IOptionsMonitor<TOptions> interface is required to update changes to the underlying ColorConsoleLoggerConfiguration object.

To control the configuration of the ColorConsoleLogger, you define an alias on its provider:

[UnsupportedOSPlatform("browser")]
[ProviderAlias("ColorConsole")]
public sealed class ColorConsoleLoggerProvider : ILoggerProvider

The ColorConsoleLoggerProvider class defines two class-scoped attributes:

The configuration can be specified with any valid configuration provider. Consider the following appsettings.json file:

{
    "Logging": {
        "ColorConsole": {
            "LogLevelToColorMap": {
                "Information": "DarkGreen",
                "Warning": "Cyan",
                "Error": "Red"
            }
        }
    }
}

This configures the log levels to the following values:

The Information log level is set to DarkGreen, which overrides the default value set in the ColorConsoleLoggerConfiguration object.

Usage and registration of the custom logger

By convention, registering services for dependency injection happens as part of the startup routine of an application. The registration occurs in the Program class, or could be delegated to a Startup class. In this example, you'll register directly from the Program.cs.

To add the custom logging provider and corresponding logger, add an ILoggerProvider with ILoggingBuilder from the HostingHostBuilderExtensions.ConfigureLogging(IHostBuilder, Action<ILoggingBuilder>):

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddColorConsoleLogger(configuration =>
{
    // Replace warning value from appsettings.json of "Cyan"
    configuration.LogLevelToColorMap[LogLevel.Warning] = ConsoleColor.DarkCyan;
    // Replace warning value from appsettings.json of "Red"
    configuration.LogLevelToColorMap[LogLevel.Error] = ConsoleColor.DarkRed;
});

using IHost host = builder.Build();

var logger = host.Services.GetRequiredService<ILogger<Program>>();

logger.LogDebug(1, "Does this line get hit?");    // Not logged
logger.LogInformation(3, "Nothing to see here."); // Logs in ConsoleColor.DarkGreen
logger.LogWarning(5, "Warning... that was odd."); // Logs in ConsoleColor.DarkCyan
logger.LogError(7, "Oops, there was an error.");  // Logs in ConsoleColor.DarkRed
logger.LogTrace(5, "== 120.");                    // Not logged

await host.RunAsync();

The ILoggingBuilder creates one or more ILogger instances. The ILogger instances are used by the framework to log the information.

The configuration from the appsettings.json file overrides the following values:

By convention, extension methods on ILoggingBuilder are used to register the custom provider:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;

public static class ColorConsoleLoggerExtensions
{
    public static ILoggingBuilder AddColorConsoleLogger(
        this ILoggingBuilder builder)
    {
        builder.AddConfiguration();

        builder.Services.TryAddEnumerable(
            ServiceDescriptor.Singleton<ILoggerProvider, ColorConsoleLoggerProvider>());

        LoggerProviderOptions.RegisterProviderOptions
            <ColorConsoleLoggerConfiguration, ColorConsoleLoggerProvider>(builder.Services);

        return builder;
    }

    public static ILoggingBuilder AddColorConsoleLogger(
        this ILoggingBuilder builder,
        Action<ColorConsoleLoggerConfiguration> configure)
    {
        builder.AddColorConsoleLogger();
        builder.Services.Configure(configure);

        return builder;
    }
}

Running this simple application will render color output to the console window similar to the following image:

Color console logger sample output

See also