Compartilhar via


Geração de origem de log em tempo de compilação

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

O gerador de origem é disparado quando LoggerMessageAttribute é usado em métodos de log partial. Quando disparado, ele é capaz de gerar automaticamente a implementação dos métodos partial aos quais está decorando ou produzir mensagens de diagnóstico no tempo de compilação com dicas sobre o uso adequado. A solução de registro em tempo de compilação é consideravelmente mais rápida em tempo de execução do que os métodos de registro existentes. Ela faz isso eliminando a conversão boxing, alocações temporárias e cópias até o limite.

Uso básico

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

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 instância ILogger é necessária como um parâmetro ou modifica a definição para usar a palavra-chave this 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 método de instância. Nesse contexto, o método de log obtém o agente acessando um campo ILogger na classe de contenção.

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 registro em log também pode obter o agente de um parâmetro de construtor primário ILogger na classe que o 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 tanto um campo ILogger quanto um parâmetro de construtor primário, o método de registro em log obterá o registrador do campo.

Às vezes, o nível de log precisa ser dinâmico, em vez de estaticamente integrado ao código. Você pode fazer isso omitindo o nível de log do atributo e, em vez disso, exigindo que ele seja 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 log e String.Empty é fornecido para a mensagem. O estado contém 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 o exemplo de saída de log ao usar o formatador JsonConsole.

{
  "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 o LoggerMessageAttribute nos métodos de log, algumas restrições devem ser seguidas:

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

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

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

Para obter mais informações, confira Controle de versão da linguagem C#.

Anatomia do método de log

A ILogger.Log assinatura aceita um LogLevel e, opcionalmente, um Exception, conforme mostrado no exemplo de código a seguir.

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, LogLevel e Exception é tratada de maneira especial 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 reporta um DiagnosticSeverity.Warning de SYSLIB0025.

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

Suporte ao nome do modelo que não diferencia 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 ILogger enumera o estado, o argumento é selecionado 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 formatador JsonConsole:

{
  "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 de parâmetro indeterminada

Não há restrições sobre a ordenação de parâmetros do método de log. Um desenvolvedor pode definir o ILogger parâmetro como o último, 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);

Dica

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 de JsonConsole a seguir 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}"
  }
}

Mais exemplos de registro em log

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

  • LogWithCustomEventName: recupere o nome do evento por meio do atributo LoggerMessage.
  • LogWithDynamicLogLevel: defina o nível de log dinamicamente para permitir que isso seja feito com base na entrada de configuração.
  • UsingFormatSpecifier: use especificadores de formato para formatar os 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 formatador SimpleConsole:

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 formatador JsonConsole:

{
  "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}"
  }
}

Redigir informações confidenciais em logs

Ao registrar dados confidenciais em log, é importante evitar a exposição acidental. Mesmo com métodos de log gerados em tempo de compilação, registrar valores confidenciais brutos pode levar a vazamentos de dados e problemas de conformidade.

A biblioteca Microsoft.Extensions.Telemetry fornece recursos avançados de enriquecimento de log e telemetria para aplicativos .NET. Ele estende o pipeline de log para aplicar automaticamente a redação a dados classificados ao gravar logs. Ele permite que você imponha políticas de proteção de dados em todo o aplicativo integrando a redação ao fluxo de trabalho de log. Ele é criado para aplicativos que precisam de informações sofisticadas de telemetria e log.

Para habilitar a redação, use a biblioteca Microsoft.Extensions.Compliance.Redaction . Essa biblioteca fornece redatores — componentes que transformam dados confidenciais (por exemplo, apagando, mascarando ou aplicando hash) para que seja seguro divulgá-los. Os redatores são selecionados com base na classificação de dados, o que permite rotular dados de acordo com sua sensibilidade (como pessoal, privado ou público).

Para usar a edição com métodos de log gerados pela fonte, você deve:

  1. Classifique seus dados confidenciais usando um sistema de classificação de dados.
  2. Registre e configure os redatores para cada classificação em seu contêiner de ID.
  3. Habilite a redação no pipeline de log.
  4. Verifique seus logs para garantir que nenhum dado confidencial seja exposto.

Por exemplo, se você tiver uma mensagem de log que tenha um parâmetro considerado privado:

[LoggerMessage(0, LogLevel.Information, "User SSN: {SSN}")]
public static partial void LogPrivateInformation(
    this ILogger logger,
    [MyTaxonomyClassifications.Private] string SSN);

Você precisará ter uma configuração semelhante a esta:

using Microsoft.Extensions.Telemetry;
using Microsoft.Extensions.Compliance.Redaction;

var services = new ServiceCollection();
services.AddLogging(builder =>
{
    // Enable redaction.
    builder.EnableRedaction();
});

services.AddRedaction(builder =>
{
    // configure redactors for your data classifications
    builder.SetRedactor<StarRedactor>(MyTaxonomyClassifications.Private);
});

public void TestLogging()
{
    LogPrivateInformation("MySSN");
}

A saída deve ser assim:

User SSN: *****

Essa abordagem garante que somente os dados redigidos sejam registrados, mesmo ao usar APIs de log geradas em tempo de compilação. Você pode usar diferentes redatores para diferentes tipos de dados ou classificações e atualizar sua lógica de redação centralmente.

Para obter mais informações sobre como classificar seus dados, consulte a classificação de dados no .NET. Para obter mais informações sobre redação e redatores, consulte Redação de Dados no .NET.

Resumo

Com o advento dos geradores de código em C#, escrever APIs de log de alta performance torna-se mais fácil. O uso da abordagem do gerador de origem tem vários benefícios importantes:

  • Permite que a estrutura de log seja preservada e habilita 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 conforme apresentados, sem complicações em relação à maneira como são armazenados antes do processamento (além de criar um string).
  • Fornece diagnósticos específicos do log, emite avisos para IDs do evento duplicadas.

Além disso, existem benefícios no uso manual de LoggerMessage.Define:

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

Confira também