Logging para bibliotecas .NET
Estabelecer mecanismos de Logging flexíveis e fáceis de implementar pode ser a diferença entre o céu e inferno ao manter várias bibliotecas numa aplicação
Logging para bibliotecas .NET está cada vez mais simples de ser implementado, há uma infinidade de opções no mercado tais como: Serilog, NLog, log4net etc.
Como vivemos um momento onde aplicações em nuvem são cada vez mais comuns, realizar o monitoramento e rastreio do que ocorre em suas entranhas é essencial.
Quem nunca passou pelo pesadelo de ter que depurar um erro sem muitos detalhes do que ocorreu e/ou sem acesso ao ambiente de produção?
Pensando nisso, a Microsoft, a cada evolução da plataforma .NET traz melhorias e ferramental mais robusto para os seus desenvolvedores.
Microsoft.Extensions.Logging
Distribuída através de um pacote Nuget, essa biblioteca provê uma série de APIs que funcionam tanto com provedores nativos .NET quanto com de terceiros.
Assim, é possível seguir diferentes abordagens para implementação do logging em suas bibliotecas.
Uma delas é a utilização de um logger específico, o que pode ser mais fácil no começo, porém obriga as aplicações que fizerem uso da sua biblioteca a usá-lo também.
Outra solução, é o uso de uma abstração de logger, tornando assim o mesmo desacoplado da sua biblioteca e fornecendo a aplicação
Usar um logger específico
Apesar da ampla quantidade de soluções prontas no disponíveis, decidir por um logger específico impossibilitaria com que os consumidores da biblioteca pudessem utilizar a de sua preferência, ou que tenham que criar complexidades adicionais para contornar essa limitação.
Usar um logger abstrato customizado
Nesse caso, é feita uma abstração do logger para que os seus consumidores possam implementá-la. Sendo que a desvantagem é que cada um deverá implementar a sua.
É claro que isso é facilmente contornado fornecendo uma implementação nativa da sua própria abstração.
Usar um logger abstrato padrão
Essa abordagem vale-se da exposição de uma abstração padrão, tal como o Microsoft.Extensions.Logging, sendo assim é possível aos consumidores tanto criarem suas implementações quanto usarem algumas já prontas.
Mão na massa no logging com o Serilog
Como já venho trabalhando com o Serilog há alguns anos, farei o uso dele nos meus exemplos. Mas como dito anteriormente, vários outros frameworks dão suporte ao uso do Microsoft.Extensions.Logging.
Instalação dos pacotes Nuget
Vamos criar uma aplicação de console nova no Visual Studio e, para a configuração inicial temos que instalar os seguintes pacotes:
Install-Package Microsoft.Extensions.Logging -Version 3.0.1 Install-Package Microsoft.Extensions.Logging.Abstractions -Version 3.0.1 Install-Package Serilog -Version 2.9.0 Install-Package Serilog.Extensions.Logging -Version 3.0.1 Install-Package Serilog.Sinks.Console -Version 3.1.1
Implementação das classes para teste
Em seguida, no Program.cs faremos a seguinte implementação:
namespace EscolaDaProgramacao.Logging { class Program { private const string ProgramName = "Playground"; static void Main(string[] args) { Log.Logger = new LoggerConfiguration() // Realiza a configuração do Serilog .MinimumLevel.Error() // Define nível mínimo das mensagens logadas .WriteTo.Console() // Habilita a saída para o Console .CreateLogger(); // Criação do Logger var loggerFactory = new LoggerFactory() .AddSerilog(); // Adiciona a config do Serilog na factory var logger = loggerFactory .CreateLogger("Playground"); // Cria um novo logger a partir da factory /* * Aqui começamos a logar as informações * Veja que fazemos uso de PlaceHolders {ProgramName}, isso é importante * para criação de logs estruturados e, também, para destacar o dado na saída */ logger.LogInformation("Iniciando o {ProgramName}", ProgramName); logger.LogDebug("Exemplo de Debug no {ProgramName}", ProgramName); logger.LogCritical("Exemplo de Critical no {ProgramName}", ProgramName); logger.LogError("Exemplo de Error no {ProgramName}", ProgramName); logger.LogWarning("Exemplo de Warning no {ProgramName}", ProgramName); logger.LogTrace("Exemplo de Trace no {ProgramName}", ProgramName); logger.LogInformation("Finalizando o {ProgramName}", ProgramName); } } }
Com esse código a saída no Console ficará como na imagem abaixo.
Ao alterarmos o MinimumLevel para Error, teremos a exclusão de alguns logs na saída.
Agora que validamos o funcionamento básico do mecanismo de logging, faremos a inclusão de uma biblioteca que receberá um ILogger em seu construtor.
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; public class LeitorContatos { private readonly ILogger _logger; public LeitorContatos(ILogger logger) { /* * Essa validação é importante, pois nem todo consumidor da biblioteca * irá fazer uso do logger. Assim, evitamos ter que testar null para * o logger a todo momento */ _logger = logger ?? NullLogger.Instance; } public void LerContatos(string nomeArquivo) { Information(nomeArquivo); Debug(nomeArquivo); Critical(nomeArquivo); Error(nomeArquivo); Warning(nomeArquivo); Trace(nomeArquivo); } private void Information(string nomeArquivo) { _logger.LogInformation("Processando arquivo {NomeArquivo}", nomeArquivo); } private void Debug(string nomeArquivo) { _logger.LogDebug("Processando a linha {LinhaArquivo} do arquivo {NomeArquivo}", 123, nomeArquivo); } private void Critical(string nomeArquivo) { _logger.LogCritical("Ocorreu uma falha ao processar a linha {LinhaArquivo} do arquivo {NomeArquivo}. O programa foi finalizado.", 987, nomeArquivo); } private void Error(string nomeArquivo) { _logger.LogError("Ocorreu um erro ao processar a linha {LinhaArquivo} do arquivo {NomeArquivo}", 987, nomeArquivo); } private void Warning(string nomeArquivo) { _logger.LogWarning("A linha {LinhaArquivo} do arquivo {NomeArquivo} tem mais caracteres do que o esperado", 35, nomeArquivo); } private void Trace(string nomeArquivo) { _logger.LogTrace("Ocorreu um erro ao processar na posição {PosicaoLinhaArquivo} a linha {LinhaArquivo} do arquivo {NomeArquivo}", 35, 1458, nomeArquivo); } }
Como podemos ver, a biblioteca não referencia nada relativo ao Serilog, apenas a nossa abstração. Isso permite o desacoplamento e reaproveitamento/utilização de várias formas de logs diferentes.
Para testarmos como será o resultado, vamos instanciar a classe LerContatos passando null como parâmetro no construtor e rodamos a aplicação.
Assim, verificamos que apesar de nada relativo a biblioteca será registrado no Console, também não temos um NullReferenceException, pois o NullLogger.Instance nos garante uma instância “falsa”.
Na sequência, iremos passar como parâmetro o logger criado. O resultado será como a imagem a seguir.
O que mais é possível?
Apesar do nosso exemplo ter sido bastante simples, acho que consiguimos ver o poder que uma estrutura de logging bem feita proporciona.
Tenha em mente o seguinte, registrar o que ocorre com sua a aplicação parece trabalhoso a princípio, assim como criar testes automatizados. Entretanto, o esforço para depurar sem informações adequadas é muito maior.
Com o uso do Serilog, nós podemos atribuir diversas saídas ao mesmo tempo para o logger, tais como escrita em arquivo no disco e o envio para ferramentas agregadoras de logs como o Stackify.
Portanto, em situações onde há limitações de infra-estrutura, por exemplo, temos como contornar isso com a aplicação de mais de um método de log.
O que achou? Você utiliza alguma ferramenta e/ou framework para estruturar seus logs? Conta pra gente nos comentários.
Os comentários estão fechados, mas trackbacks E pingbacks estão abertos.