Implementar um provedor de log personalizado no .NET

Há muitos provedores de log disponíveis para necessidades comuns de log. Talvez seja necessário implementar um ILoggerProvider personalizado quando um dos provedores disponíveis não atender às necessidades do aplicativo. Neste artigo, você aprenderá a implementar um provedor de log personalizado que pode ser usado para colorir logs no console.

Dica

O código-fonte de exemplo do provedor de log personalizado está disponível no repositório Docs Github. Saiba mais em GitHub: .NET Docs - Log personalizado do console.

Configuração de agente personalizado de amostra

O exemplo cria diferentes entradas de console de cores por nível de log e ID de evento usando o seguinte tipo de configuração:

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
    };
}

O código anterior define o nível padrão como Information, a cor para Green, e o EventId é implicitamente 0.

Criar o agente personalizado

O nome da categoria de implementação ILogger normalmente é a origem do log. Por exemplo, o tipo em que o agente é criado:

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();
        }
    }
}

O código anterior:

  • Cria uma instância do agente por nome de categoria.
  • Verifica _getCurrentConfig().LogLevelToColorMap.ContainsKey(logLevel) em IsEnabled, para que cada logLevel tenha um agente exclusivo. Nesta implementação, cada nível de log requer uma entrada de configuração explícita para o log.

É uma boa prática chamar ILogger.IsEnabled em implementações ILogger.Log, pois Log pode ser chamado por qualquer consumidor e não há garantias de que ele foi verificado anteriormente. O método IsEnabled deve ser muito rápido na maioria das implementações.

TState state,
Exception? exception,

O agente é instanciado com o name e um Func<ColorConsoleLoggerConfiguration>, que retorna a configuração atual. Isso trata das atualizações para os valores de configuração conforme monitorado por meio do retorno de chamada IOptionsMonitor<TOptions>.OnChange.

Importante

A implementação ILogger.Log verifica se o valor config.EventId está definido. Quando config.EventId não está definido ou quando corresponde ao logEntry.EventId exato, o agente registra na cor.

Provedor de agente personalizado

O objeto ILoggerProvider é responsável pela criação de instâncias do agente. Não é necessário criar uma instância de agente por categoria, mas faz sentido para alguns agentes, como NLog ou log4net. Essa estratégia permite que você escolha diferentes destinos de saída de log por categoria, como no exemplo a seguir:

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();
    }
}

No código anterior, CreateLogger cria uma única instância do ColorConsoleLogger por nome de categoria e a armazena no ConcurrentDictionary<TKey,TValue>. Além disso, a interface IOptionsMonitor<TOptions> é necessária para atualizar as alterações no objeto ColorConsoleLoggerConfiguration subjacente.

Para controlar a configuração do ColorConsoleLogger, defina um alias em seu provedor:

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

A classe ColorConsoleLoggerProvider define dois atributos com escopo de classe:

A configuração pode ser especificada com qualquer provedor de configuração válido. Considere o seguinte arquivo appsettings.json:

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

Ele configura os níveis de log para os seguintes valores:

O nível de log Information é definido como DarkGreen, o que substitui o valor padrão definido no objeto ColorConsoleLoggerConfiguration.

Uso e registro do agente personalizado

Por convenção, o registro de serviços para injeção de dependência acontece como parte da rotina de inicialização de um aplicativo. O registro ocorre na classe Program ou pode ser delegado a uma classe Startup. Nesse exemplo, você se registrará diretamente no Program.cs.

Para adicionar o provedor de log personalizado e o agente correspondente, adicione um ILoggerProvider com ILoggingBuilder do 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();

O ILoggingBuilder cria uma ou mais instâncias ILogger. As instâncias ILogger são usadas pela estrutura para registrar as informações em log.

A configuração do arquivo appsettings.json substitui os seguintes valores:

Por convenção, os métodos de extensão no ILoggingBuilder são usados para registrar o provedor personalizado:

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;
    }
}

A execução deste aplicativo simples renderizará a saída de cores para a janela do console semelhante à seguinte imagem:

Color console logger sample output

Confira também