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)
inIsEnabled
, so eachlogLevel
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:
- UnsupportedOSPlatformAttribute: The
ColorConsoleLogger
type is not supported in the"browser"
. - ProviderAliasAttribute: Configuration sections can define options using the
"ColorConsole"
key.
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:
- LogLevel.Information: ConsoleColor.DarkGreen
- LogLevel.Warning: ConsoleColor.Cyan
- LogLevel.Error: ConsoleColor.Red
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: