Geração de código-fonte de log em tempo de compilação

O .NET 6 introduz o LoggerMessageAttribute tipo. Esse atributo faz parte do Microsoft.Extensions.Logging namespace e, quando usado, gera APIs de log de alto desempenho. O suporte de log de geração de origem foi projetado para fornecer uma solução de log altamente utilizável e de alto desempenho para aplicativos .NET modernos. O código-fonte gerado automaticamente depende da interface em conjunto com LoggerMessage.Define a ILogger funcionalidade.

O gerador de origem é acionado quando LoggerMessageAttribute é usado em partial métodos de registro. Quando acionado, ele é capaz de gerar automaticamente a partial implementação dos métodos que está decorando ou produzir diagnósticos em tempo de compilação com dicas sobre o uso adequado. A solução de log em tempo de compilação normalmente é consideravelmente mais rápida em tempo de execução do que as abordagens de log existentes. Ele consegue isso eliminando o boxe, alocações temporárias e cópias na máxima extensão possível.

Utilização básica

Para usar o LoggerMessageAttribute, a classe e o método de consumo precisam ser partial. O gerador de código é acionado em tempo de compilação e gera uma implementação do partial método.

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        ILogger logger, string hostName);
}

No exemplo anterior, o método de log é static e o nível de log é especificado na definição de atributo. Ao usar o atributo em um contexto estático, a ILogger instância é necessária como um parâmetro ou modifica a definição para usar a this palavra-chave para definir o método como um método de extensão.

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        this ILogger logger, string hostName);
}

Você também pode optar por usar o atributo em um contexto não estático. Considere o exemplo a seguir em que o método de log é declarado como um método de instância. Nesse contexto, o método de log obtém o logger acessando um ILogger campo na classe que contém.

public partial class InstanceLoggingExample
{
    private readonly ILogger _logger;

    public InstanceLoggingExample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public partial void CouldNotOpenSocket(string hostName);
}

A partir do .NET 9, o método de log também pode obter o logger de um ILogger parâmetro de construtor primário na classe que contém.

public partial class InstanceLoggingExample(ILogger logger)
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public partial void CouldNotOpenSocket(string hostName);
}

Se houver um ILogger parâmetro field e um parâmetro primário do construtor, o método de log obterá o logger do campo.

Às vezes, o nível de log precisa ser dinâmico em vez de incorporado estaticamente no código. Você pode fazer isso omitindo o nível de log do atributo e, em vez disso, exigindo-o como um parâmetro para o método de log.

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        ILogger logger,
        LogLevel level, /* Dynamic log level as parameter, rather than defined in attribute. */
        string hostName);
}

Você pode omitir a mensagem de registro e String.Empty será fornecido para a mensagem. O estado conterá os argumentos, formatados como pares chave-valor.

using System.Text.Json;
using Microsoft.Extensions.Logging;

using ILoggerFactory loggerFactory = LoggerFactory.Create(
    builder =>
    builder.AddJsonConsole(
        options =>
        options.JsonWriterOptions = new JsonWriterOptions()
        {
            Indented = true
        }));

ILogger<SampleObject> logger = loggerFactory.CreateLogger<SampleObject>();
logger.PlaceOfResidence(logLevel: LogLevel.Information, name: "Liana", city: "Seattle");

readonly file record struct SampleObject { }

public static partial class Log
{
    [LoggerMessage(EventId = 23, Message = "{Name} lives in {City}.")]
    public static partial void PlaceOfResidence(
        this ILogger logger,
        LogLevel logLevel,
        string name,
        string city);
}

Considere a saída de log de exemplo ao usar o JsonConsole formatador.

{
  "EventId": 23,
  "LogLevel": "Information",
  "Category": "\u003CProgram\u003EF...9CB42__SampleObject",
  "Message": "Liana lives in Seattle.",
  "State": {
    "Message": "Liana lives in Seattle.",
    "name": "Liana",
    "city": "Seattle",
    "{OriginalFormat}": "{Name} lives in {City}."
  }
}

Restrições do método de log

Ao usar os LoggerMessageAttribute métodos de registro em log, algumas restrições devem ser seguidas:

  • Os métodos de registro devem ser partial e retornar void.
  • Os nomes dos métodos de registro em log não devem começar com um sublinhado.
  • Os nomes de parâmetros dos métodos de log não devem começar com um sublinhado.
  • Os métodos de registro em log não podem ser definidos em um tipo aninhado.
  • Os métodos de registo não podem ser genéricos.
  • Se um método de log for static, a ILogger instância será necessária como um parâmetro.

O modelo de geração de código depende do código que está sendo compilado com um compilador C# moderno, versão 9 ou posterior. O compilador C# 9.0 ficou disponível com o .NET 5. Para atualizar para um compilador C# moderno, edite seu arquivo de projeto para C# 9.0 de destino.

<PropertyGroup>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>

Para obter mais informações, consulte Versionamento de linguagem C#.

Anatomia do método logarítmico

A ILogger.Log assinatura aceita o LogLevel e, opcionalmente, um Exception, como mostrado abaixo.

public interface ILogger
{
    void Log<TState>(
        Microsoft.Extensions.Logging.LogLevel logLevel,
        Microsoft.Extensions.Logging.EventId eventId,
        TState state,
        System.Exception? exception,
        Func<TState, System.Exception?, string> formatter);
}

Como regra geral, a primeira instância de ILogger, LogLevele Exception são tratados especialmente na assinatura do método de log do gerador de origem. As instâncias subsequentes são tratadas como parâmetros normais para o modelo de mensagem:

// This is a valid attribute usage
[LoggerMessage(
    EventId = 110, Level = LogLevel.Debug, Message = "M1 {Ex3} {Ex2}")]
public static partial void ValidLogMethod(
    ILogger logger,
    Exception ex,
    Exception ex2,
    Exception ex3);

// This causes a warning
[LoggerMessage(
    EventId = 0, Level = LogLevel.Debug, Message = "M1 {Ex} {Ex2}")]
public static partial void WarningLogMethod(
    ILogger logger,
    Exception ex,
    Exception ex2);

Importante

Os avisos emitidos fornecem detalhes sobre o uso correto do LoggerMessageAttribute. No exemplo anterior, o WarningLogMethod irá relatar a DiagnosticSeverity.Warning de SYSLIB0025.

Don't include a template for `ex` in the logging message since it is implicitly taken care of.

Suporte a nomes de modelos que não diferenciam maiúsculas de minúsculas

O gerador faz uma comparação que não diferencia maiúsculas de minúsculas entre itens no modelo de mensagem e nomes de argumento na mensagem de log. Isso significa que, quando o ILogger enumera o estado, o argumento é captado pelo modelo de mensagem, o que pode tornar os logs mais agradáveis de consumir:

public partial class LoggingExample
{
    private readonly ILogger _logger;

    public LoggingExample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 10,
        Level = LogLevel.Information,
        Message = "Welcome to {City} {Province}!")]
    public partial void LogMethodSupportsPascalCasingOfNames(
        string city, string province);

    public void TestLogging()
    {
        LogMethodSupportsPascalCasingOfNames("Vancouver", "BC");
    }
}

Considere o exemplo de saída de log ao usar o JsonConsole formatador:

{
  "EventId": 13,
  "LogLevel": "Information",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "City": "Vancouver",
    "Province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}

Ordem indeterminada dos parâmetros

Não há restrições na ordenação dos parâmetros do método log. Um desenvolvedor pode definir o ILogger como o último parâmetro, embora possa parecer um pouco estranho.

[LoggerMessage(
    EventId = 110,
    Level = LogLevel.Debug,
    Message = "M1 {Ex3} {Ex2}")]
static partial void LogMethod(
    Exception ex,
    Exception ex2,
    Exception ex3,
    ILogger logger);

Gorjeta

A ordem dos parâmetros em um método de log não é necessária para corresponder à ordem dos espaços reservados do modelo. Em vez disso, espera-se que os nomes de espaço reservado no modelo correspondam aos parâmetros. Considere a saída a seguir JsonConsole e a ordem dos erros.

{
  "EventId": 110,
  "LogLevel": "Debug",
  "Category": "ConsoleApp.Program",
  "Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
  "State": {
    "Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
    "ex2": "System.Exception: This is the second error.",
    "ex3": "System.Exception: Third time's the charm.",
    "{OriginalFormat}": "M1 {Ex3} {Ex2}"
  }
}

Exemplos de registo adicionais

Os exemplos a seguir demonstram como recuperar o nome do evento, definir o nível de log dinamicamente e formatar parâmetros de log. Os métodos de registro são:

  • LogWithCustomEventName: Recupere o nome do evento via LoggerMessage atributo.
  • LogWithDynamicLogLevel: Defina o nível de log dinamicamente, para permitir que o nível de log seja definido com base na entrada de configuração.
  • UsingFormatSpecifier: Use especificadores de formato para formatar parâmetros de log.
public partial class LoggingSample
{
    private readonly ILogger _logger;

    public LoggingSample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 20,
        Level = LogLevel.Critical,
        Message = "Value is {Value:E}")]
    public static partial void UsingFormatSpecifier(
        ILogger logger, double value);

    [LoggerMessage(
        EventId = 9,
        Level = LogLevel.Trace,
        Message = "Fixed message",
        EventName = "CustomEventName")]
    public partial void LogWithCustomEventName();

    [LoggerMessage(
        EventId = 10,
        Message = "Welcome to {City} {Province}!")]
    public partial void LogWithDynamicLogLevel(
        string city, LogLevel level, string province);

    public void TestLogging()
    {
        LogWithCustomEventName();

        LogWithDynamicLogLevel("Vancouver", LogLevel.Warning, "BC");
        LogWithDynamicLogLevel("Vancouver", LogLevel.Information, "BC");

        UsingFormatSpecifier(logger, 12345.6789);
    }
}

Considere o exemplo de saída de log ao usar o SimpleConsole formatador:

trce: LoggingExample[9]
      Fixed message
warn: LoggingExample[10]
      Welcome to Vancouver BC!
info: LoggingExample[10]
      Welcome to Vancouver BC!
crit: LoggingExample[20]
      Value is 1.234568E+004

Considere o exemplo de saída de log ao usar o JsonConsole formatador:

{
  "EventId": 9,
  "LogLevel": "Trace",
  "Category": "LoggingExample",
  "Message": "Fixed message",
  "State": {
    "Message": "Fixed message",
    "{OriginalFormat}": "Fixed message"
  }
}
{
  "EventId": 10,
  "LogLevel": "Warning",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "city": "Vancouver",
    "province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}
{
  "EventId": 10,
  "LogLevel": "Information",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "city": "Vancouver",
    "province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}
{
  "EventId": 20,
  "LogLevel": "Critical",
  "Category": "LoggingExample",
  "Message": "Value is 1.234568E+004",
  "State": {
    "Message": "Value is 1.234568E+004",
    "value": 12345.6789,
    "{OriginalFormat}": "Value is {Value:E}"
  }
}

Resumo

Com o advento dos geradores de código-fonte C#, escrever APIs de log de alto desempenho é muito mais fácil. O uso da abordagem do gerador de código-fonte tem vários benefícios principais:

  • Permite que a estrutura de log seja preservada e permite a sintaxe de formato exato exigida pelos Modelos de Mensagem.
  • Permite fornecer nomes alternativos para os espaços reservados do modelo e usar especificadores de formato.
  • Permite a passagem de todos os dados originais no estado em que se encontram, sem qualquer complicação em torno de como eles são armazenados antes que algo seja feito com eles (além de criar um string).
  • Fornece diagnósticos específicos de registro em log e emite avisos para IDs de eventos duplicados.

Além disso, há benefícios em usar manualmente LoggerMessage.Define:

  • Sintaxe mais curta e simples: uso declarativo de atributos em vez de codificação clichê.
  • Experiência guiada do desenvolvedor: o gerador dá avisos para ajudar os desenvolvedores a fazer a coisa certa.
  • Suporte para um número arbitrário de parâmetros de log. LoggerMessage.Define suporta um máximo de seis.
  • Suporte para nível de log dinâmico. Isso não é possível sozinho LoggerMessage.Define .

Consulte também