Reduzindo drasticamente o uso de memória com o Log. (Parte 2)
Tags: dotnet, dotnetcore, dotnetframework, aspdotnet
Published: 2023-07-14 12:30:09

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:

```csharp
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.

```csharp
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.

```csharp
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.

```csharp
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
![benchmark](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62kk9hmmsxgi8uaan2ef.png)

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](https://learn.microsoft.com/en-us/dotnet/core/extensions/high-performance-logging)