Como visto no artigo anterior, escrever logs é uma prática comum no desenvolvimento de aplicações .NET, pois permite monitorar o comportamento ou depurar as aplicações. No entanto, existem abordagens mais eficientes e que proporcionam um melhor desempenho ao escrever logs.
Neste post, faremos uma comparação entre o Logger comum e o LoggerMessage, analisando as vantagens e desvantagens de cada um.
O Logger comum é aquele que utiliza os métodos de extensão da interface ILogger, como LogInformation, LogDebug, LogError, entre outros. Esses métodos são simples e convenientes de utilizar, porém apresentam algumas desvantagens:
- Eles exigem a conversão boxing de tipos de valor, como int, em object. Isso implica em alocações de memória desnecessárias e custos de cópia.
- A análise do modelo de mensagem (cadeia de caracteres de formato nomeada) é realizada toda vez que uma mensagem de log é gravada. Isso acarreta custos computacionais adicionais e potenciais erros de formatação.
- Não é possível personalizar dinamicamente o nível de log, uma vez que ele é definido estaticamente no código.
Por outro lado, o LoggerMessage é uma classe que oferece funcionalidades para criar delegates que podem ser armazenados em cache, resultando em menos alocações de objetos e menor sobrecarga computacional em comparação com os métodos de extensão do Logger. Para cenários de registro de logs com alto desempenho, recomenda-se o uso do padrão LoggerMessage, que apresenta as seguintes vantagens:
- Evita a conversão boxing por meio do uso de campos definidos nos actions (Action) estáticos e métodos de extensão com parâmetros fortemente tipados.
- A análise do modelo de mensagem é realizada apenas uma vez, no momento em que a mensagem é definida, e não a cada chamada de log.
- Permite especificar dinamicamente o nível de log como um parâmetro do método de log.
Para utilizar o LoggerMessage, é necessário definir um delegate Action utilizando o método Define da classe LoggerMessage, informando o nível de log, o ID do evento e o modelo da mensagem. Em seguida, o delegate pode ser invocado passando a instância do ILogger e os parâmetros da mensagem. Por exemplo:
public static class LoggerMessageExtension
{
private static readonly Action<ILogger, int, Exception?> LoggerMessageInformation =
LoggerMessage.Define<int>(
LogLevel.Information,
0,
"Gerando LogInformation : O pedido de número {Pedido} foi criado!");
public static void LogMessageInformation(this ILogger logger, int pedido)
{
LoggerMessageInformation(logger, pedido, null);
}
}
No exemplo acima, definimos um delegate estático para registrar uma mensagem informativa sobre um número de pedido, utilizando um parâmetro inteiro para o número do pedido. Em seguida, criamos um método estático para invocar esse delegate, passando a instância do ILogger e o número do pedido.
Uma alternativa mais conveniente para utilizar o LoggerMessage é através do atributo LoggerMessageAttribute. Esse atributo faz parte do namespace Microsoft.Extensions.Logging e, ao ser utilizado, gera APIs de log de alto desempenho em tempo de compilação. A geração de APIs de log é projetada para fornecer uma solução de log altamente eficiente e fácil de usar.
O código gerado automaticamente depende da interface ILogger em conjunto com a funcionalidade LoggerMessage.Define. O gerador de APIs de log é acionado quando o LoggerMessageAttribute é utilizado em métodos de log parciais. Ao ser acionado, ele é capaz de gerar automaticamente a implementação dos métodos parciais que estão sendo decorados ou fornecer diagnósticos em tempo de compilação com orientações sobre o uso adequado.
A solução de log em tempo de compilação é geralmente mais rápida em tempo de execução do que as abordagens de log existentes. Isso é alcançado eliminando o boxing, as alocações temporárias e as cópias sempre que possível.
Para utilizar o LoggerMessageAttribute, a classe e o método que o consomem precisam ser declarados como parciais. O Source Generator é acionado durante a compilação e gera uma implementação do método parcial.
public static partial class LoggerMessageExtensionSourceGenerator
{
[LoggerMessage(EventId = 1,
Level = LogLevel.Information,
Message = "Pedido {Pedido} gerado com sucesso!")]
public static partial void LogMessageInformationSource(ILogger logger, int pedido);
}
No exemplo anterior, o método de log é estático e o nível de log é especificado na definição do atributo. Ao utilizar o atributo em um contexto estático, é necessário passar a instância do ILogger como um parâmetro ou modificar a definição do método para usar a palavra-chave "this" e torná-lo um método de extensão.
public static partial class LoggerMessageExtensionSourceGenerator
{
[LoggerMessage(EventId = 1,
Level = LogLevel.Information,
Message = "Pedido {Pedido} gerado com sucesso!")]
public static partial void LogMessageInformationSource(this ILogger logger, int pedido);
}
Às vezes, é necessário que o nível de log seja dinâmico em vez de ser definido estaticamente no código. Isso pode ser alcançado omitindo o nível de log no atributo e exigindo-o como um parâmetro para o método de log.
public static partial class LoggerMessageExtensionSourceGenerator
{
[LoggerMessage(
EventId = 1,
Message = "Pedido {Pedido} gerado com sucesso!")]
public static partial void LogMessageInformationSource(
this ILogger logger,
LogLevel level, /* Nível de log dinâmico como parâmetro, em vez de definido no atributo. */
int pedido);
}
Benchmark
Observando os resultados acima (loop com 100.000 interações), fica evidente que o método LogInformation do Logger comum tende a alocar memória e ser mais lento em comparação com o LoggerMessage (22x mais rápido e sem alocação) e o Source Generator associado a ele (26x mais rápido e sem alocação).
Conclusão
O Logger comum e o LoggerMessage são duas abordagens para registrar informações no .NET, cada uma com suas vantagens e desvantagens. O Logger comum é mais simples e intuitivo, mas pode resultar em custos de desempenho adicionais e possíveis erros de formatação. Por outro lado, o LoggerMessage é mais eficiente e oferece uma abordagem mais segura, mas requer um pouco mais de código e pode ser menos legível.
A escolha entre o Logger comum e o LoggerMessage depende do cenário específico e das preferências do desenvolvedor. Se o desempenho é uma preocupação importante e é necessário evitar alocações desnecessárias de memória, o LoggerMessage pode ser a melhor opção. Ele oferece a possibilidade de pré-definir delegates que podem ser armazenados em cache e reutilizados, minimizando o impacto no desempenho. Além disso, permite personalizar dinamicamente o nível de log.
Por outro lado, se a simplicidade e a conveniência são prioridades, o Logger comum pode ser mais adequado. Ele oferece métodos de extensão simples de usar e não requer a definição prévia de delegates. No entanto, é importante estar ciente dos possíveis custos de desempenho e erros de formatação associados a essa abordagem.
Em resumo, a escolha entre o Logger comum e o LoggerMessage depende do equilíbrio entre desempenho, facilidade de uso e preferências pessoais. Ambas as abordagens têm seu lugar no desenvolvimento de aplicações .NET, e é importante considerar o contexto específico ao decidir qual utilizar.
Referência:
High-performance logging in .NET
Top comments (0)