Logs no ASP.NET com Serilog

O Serilog é uma biblioteca de logs para aplicações .NET que organiza tudo em torno de destinos plugáveis, chamados de sinks. Neste artigo, vamos configurar o pacote em uma aplicação ASP.NET, gravar mensagens no console, em arquivo e por e-mail, e entender como escolher o destino certo para cada necessidade.

Logs são a memória de uma aplicação em execução. Quando algo dá errado em produção, eles costumam ser a única pista disponível para reconstruir o que aconteceu.

O que é o Serilog

O Serilog é uma estrutura que permite salvar qualquer tipo de informação, de forma organizada, durante a execução da aplicação. Além disso, ele funciona em qualquer tipo de projeto .NET, seja uma API, um worker ou uma aplicação de console.

O grande diferencial da biblioteca é o log estruturado. Em vez de gravar apenas uma frase pronta, o Serilog guarda a mensagem e também os valores que a compõem como propriedades separadas.

Na prática, isso significa que você consegue filtrar e pesquisar os eventos por campo, por exemplo, todos os pedidos de um cliente específico. Por isso, ferramentas como Seq, Elasticsearch e Application Insights aproveitam muito bem os eventos gerados por ele.

Sinks: os destinos dos logs

Quando trabalhamos com logs, os gostos e as necessidades variam muito. Cada empresa ou time opta por concentrar seus registros em um local diferente, e o Serilog ajuda exatamente nisso.

O núcleo da biblioteca traz apenas a implementação básica do pipeline de logs. Os sinks, por sua vez, fazem todo o trabalho pesado de entregar cada evento ao destino final.

Dessa forma, caso queira que os logs sejam gravados em um arquivo, basta utilizar o sink de arquivo. O mesmo acontece para e-mail, com o sink de e-mail, e até para o Discord, com um sink específico para ele.

Você também pode criar o seu próprio sink, caso precise armazenar os logs em algum local da sua empresa que os sinks existentes não atendam. Para isso, basta implementar a interface ILogEventSink e registrar a classe na configuração.

Como o Serilog se integra ao ASP.NET

O ASP.NET já possui uma abstração de logs própria, baseada na interface ILogger. Segundo a documentação oficial de logs do ASP.NET Core, o framework permite trocar ou complementar os provedores padrão por provedores de terceiros.

O Serilog aparece nessa lista de provedores de terceiros recomendados. Assim, ao plugar a biblioteca no host, todo log emitido pelo próprio framework e pelo seu código via ILogger passa a ser processado pelo Serilog.

Vale lembrar que o ASP.NET não traz um provedor nativo para gravar logs em arquivo. Esse é justamente um dos motivos pelos quais o Serilog se tornou tão popular no ecossistema.

Instalando o Serilog no projeto

O primeiro passo para utilizar o Serilog é instalar o pacote principal de integração com o ASP.NET, do qual os demais recursos derivam. Para isso, execute o comando abaixo na pasta do projeto.

dotnet add package Serilog.AspNetCore

Esse pacote já traz como dependência os sinks de console, de arquivo e de debug, além do suporte a configuração via appsettings.json. Portanto, para os primeiros exemplos deste artigo, nenhuma instalação adicional é necessária.

Em seguida, precisamos adicionar o UseSerilog no Host da aplicação, dentro do Program.cs, conforme mostrado abaixo.

var builder = WebApplication.CreateBuilder(args);

// As configurações serão preenchidas nas próximas seções
builder.Host.UseSerilog(...);

É importante notar que, neste caso, deixamos as configurações vazias por enquanto (...). Consequentemente, o código não compila se tentarmos executá-lo assim.

A seguir, veremos como configurar os sinks da maneira correta para diferentes tipos de log.

Logs no console com Serilog

A forma mais simples de acompanhar o que acontece durante a execução é observar o console, ou seja, o terminal onde a aplicação está rodando. Por isso, ele costuma ser o primeiro sink configurado em qualquer projeto.

Para enviar as informações ao console com o Serilog, configuramos o método WriteTo apontando para Console.

using Serilog;
using Serilog.Events;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration
    .MinimumLevel.Debug()
    .WriteTo.Console(LogEventLevel.Debug));

var app = builder.Build();
app.Run();

Note que neste exemplo utilizamos o LogEventLevel.Debug no sink. Esse parâmetro define o nível mínimo que o console aceita, então ele exibe mensagens de Debug e de todos os níveis acima dele.

Além disso, repare na chamada MinimumLevel.Debug(). O nível mínimo padrão do Serilog é Information, então sem essa linha as mensagens de Debug seriam descartadas antes mesmo de chegar ao sink.

A partir deste ponto, todas as mensagens de Debug, incluindo as do próprio .NET, aparecem no terminal. O mesmo vale para Warnings e Errors: basta ajustar o LogEventLevel para controlar o que será exibido.

Níveis de log do Serilog

O Serilog trabalha com seis níveis, do mais detalhado ao mais crítico. Entender essa ordem ajuda a decidir o que cada destino deve receber.

  • Verbose: rastreamento minucioso, raramente ligado em produção.
  • Debug: detalhes internos úteis durante o desenvolvimento.
  • Information: eventos normais do fluxo, como uma requisição concluída.
  • Warning: algo inesperado aconteceu, mas a aplicação seguiu em frente.
  • Error: uma operação falhou e precisa de atenção.
  • Fatal: uma falha grave que derruba a aplicação ou parte dela.

Quando um sink recebe um nível mínimo, ele aceita aquele nível e todos os que vêm depois dele na lista. Assim, um sink configurado com Warning registra também Error e Fatal.

Mensagens customizadas

Caso queira disparar alguma mensagem própria durante a execução, você pode utilizar a classe estática Log, do namespace Serilog. Basta escolher o método do nível desejado e informar o texto.

using Serilog;

Log.Error("Error message");
Log.Information("Information message");
Log.Fatal("Fatal error");
Log.Debug("Debug message");
Log.Warning("Warning message");

Em um projeto ASP.NET, no entanto, a abordagem mais comum é injetar o ILogger<T> nas classes. Com o Serilog plugado no host, essas chamadas também passam pelos sinks configurados.

Templates de mensagem e log estruturado

O Serilog interpreta as mensagens como templates. Em vez de concatenar strings, você escreve marcadores entre chaves e passa os valores como argumentos.

var orderId = 1042;
var customerEmail = "customer@example.com";

// Cada marcador vira uma propriedade do evento
Log.Information("Order {OrderId} created for {CustomerEmail}", orderId, customerEmail);

No console, a mensagem aparece completa e legível. Por outro lado, em destinos estruturados, OrderId e CustomerEmail ficam disponíveis como campos independentes para buscas e filtros.

Por esse motivo, evite interpolar strings com $"" nas mensagens de log. Ao fazer isso, você perde as propriedades e cada mensagem passa a ser um texto único, difícil de agrupar.

Logs em arquivo com Serilog

Outro destino muito útil é o Serilog.Sinks.File, que grava as mensagens em um arquivo de texto. Particularmente, este é um dos sinks que mais utilizo no dia a dia.

builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration
    .WriteTo.Console(LogEventLevel.Debug)
    .WriteTo.File("log.txt",
        LogEventLevel.Warning,
        rollingInterval: RollingInterval.Day));

Neste exemplo, o arquivo recebe apenas Warnings e níveis acima, enquanto o console continua exibindo tudo. Dessa forma, o disco guarda só o que realmente merece investigação.

O parâmetro rollingInterval cria um novo arquivo a cada dia. Com isso, o sink acrescenta a data ao nome configurado, gerando arquivos como log20220315.txt.

Também é possível controlar quantos arquivos ficam guardados com retainedFileCountLimit e limitar o tamanho de cada um com fileSizeLimitBytes. Assim, os logs não crescem indefinidamente e não lotam o disco do servidor.

Logs por e-mail com Serilog

Seguindo a mesma linha de raciocínio, podemos receber as mensagens por e-mail. Nesse caso, porém, é preciso um cuidado especial para não lotar a caixa de entrada com notificações.

O Serilog possui um sink dedicado para essa tarefa, e podemos instalá-lo pelo pacote abaixo.

dotnet add package Serilog.Sinks.Email

Feito isso, basta configurar o sink no Program.cs, como fizemos anteriormente com o console.

using System.Net;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Email;

builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration
    .WriteTo.Console(LogEventLevel.Debug)
    .WriteTo.Email(new EmailConnectionInfo
    {
        Port = 587,
        EmailSubject = "TEST",
        EnableSsl = true,
        FromEmail = "hello@balta.io",
        MailServer = "smtp.sendgrid.net",
        NetworkCredentials = new NetworkCredential("apikey", "pwd"),
        ToEmail = "hello@balta.io",
        IsBodyHtml = true
    }));

Nesse exemplo, usamos o SMTP do SendGrid, mas qualquer servidor de e-mail funciona. Basta ajustar o endereço, a porta e as credenciais.

Para evitar excesso de mensagens, defina um nível mínimo alto para esse sink, como Error ou Fatal. Afinal, o e-mail deve sinalizar problemas que exigem ação imediata, e não registrar cada requisição.

Também vale observar que versões mais recentes do Serilog.Sinks.Email reorganizaram as classes de configuração. Portanto, confira o README da versão instalada antes de copiar o exemplo.

Configurando o Serilog pelo appsettings.json

Até aqui, definimos tudo diretamente no código. Contudo, em produção é comum ajustar níveis e destinos sem precisar recompilar a aplicação.

Para isso, o Serilog lê a configuração do próprio appsettings.json com o método ReadFrom.Configuration.

builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration
    .ReadFrom.Configuration(context.Configuration));

Em seguida, criamos uma seção chamada Serilog no arquivo de configuração.

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": { "path": "log.txt", "rollingInterval": "Day" }
      }
    ]
  }
}

O bloco Override reduz o volume de mensagens do próprio framework, que costuma ser bastante falante. Com isso, os seus logs de negócio ficam em evidência e não se perdem no meio do ruído.

Enriquecendo os eventos

Além dos sinks, o Serilog trabalha com enrichers, que acrescentam propriedades a todos os eventos de forma automática. Por exemplo, você pode marcar cada log com o nome da aplicação ou do ambiente.

builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "Store.Api")
    .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
    .WriteTo.Console());

O FromLogContext permite adicionar propriedades temporárias dentro de um bloco de código. Assim, todos os eventos gerados naquele trecho carregam o mesmo identificador, o que facilita bastante a investigação.

using Serilog.Context;

// Todos os logs dentro do using recebem a propriedade OrderId
using (LogContext.PushProperty("OrderId", 1042))
{
    Log.Information("Processing payment");
    Log.Information("Payment approved");
}

Quando vários serviços enviam logs para o mesmo destino, essas propriedades fazem toda a diferença. Afinal, elas permitem separar rapidamente o que veio de cada aplicação e de cada ambiente.

Capturando falhas na inicialização

Existe um ponto cego comum: erros que acontecem antes de o host terminar de subir. Nesse momento, o UseSerilog ainda não foi aplicado e a exceção pode sumir sem deixar rastro.

Para resolver isso, o Serilog permite criar um logger inicial logo na primeira linha do Program.cs. Em seguida, envolvemos a inicialização em um bloco try.

using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateBootstrapLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration
        .ReadFrom.Configuration(context.Configuration));

    var app = builder.Build();
    app.Run();
}
catch (Exception exception)
{
    Log.Fatal(exception, "Application failed to start");
}
finally
{
    // Garante que os eventos pendentes sejam gravados
    Log.CloseAndFlush();
}

O CreateBootstrapLogger gera um logger provisório, que depois é substituído pela configuração completa. Dessa forma, nenhuma falha de inicialização passa despercebida.

Logs de requisição e boas práticas

O pacote de integração também oferece o middleware UseSerilogRequestLogging. Ele condensa as informações de cada requisição HTTP em um único evento, com método, caminho, status e tempo de resposta.

var app = builder.Build();

// Registra um evento resumido por requisição
app.UseSerilogRequestLogging();

app.MapGet("/", () => "Hello World!");

app.Run();

Como todo middleware, a posição dele no pipeline importa. Se quiser entender melhor esse conceito, veja o artigo sobre middleware no ASP.NET e como criar o seu.

Outra boa prática é garantir que os eventos pendentes sejam gravados antes de a aplicação encerrar. Para isso, chame Log.CloseAndFlush() no fim do Program.cs, principalmente quando usar sinks que trabalham com buffer.

Além disso, nunca registre senhas, tokens ou dados pessoais sensíveis. Logs costumam ser lidos por muitas pessoas e armazenados por bastante tempo, então trate-os como dados que podem vazar.

Por fim, combine os logs com um tratamento global de erros. O artigo sobre IExceptionHandler e tratamento global de exceções mostra como centralizar essas falhas em um único ponto.

Outros sinks do Serilog

Algo que gosto muito no Serilog é o ótimo suporte da comunidade. É quase impossível precisar de um sink que ainda não exista, e você pode conferir a lista completa de sinks disponíveis na wiki do projeto.

Entre os mais usados estão os sinks para Seq, Elasticsearch, Application Insights, bancos de dados relacionais e serviços de mensageria. Em todos os casos, o processo segue o mesmo padrão: instalar o pacote e adicionar um novo WriteTo.

Aqui no balta, por exemplo, utilizamos o sink do Discord, já que concentramos boa parte das nossas informações por lá. É muito prático ver o que está acontecendo no site e ainda poder agir de forma proativa.

Como cada WriteTo é independente, você pode combinar vários destinos ao mesmo tempo. Por exemplo, o console exibe tudo, o arquivo guarda Warnings e o Discord recebe apenas Errors.

Antes de adotar um sink da comunidade, porém, verifique se o pacote continua recebendo atualizações e se é compatível com a versão do Serilog do seu projeto. Um sink abandonado pode falhar silenciosamente e fazer você perder justamente os eventos mais importantes.

Também vale avaliar o custo de cada destino. Serviços pagos de observabilidade costumam cobrar pelo volume ingerido, então níveis mínimos bem definidos ajudam a controlar a fatura.

Conclusão

Escrever logs de forma simples, com extensões e suporte para múltiplos formatos e plataformas no .NET, é tarefa para o Serilog. Ele é um dos meus pacotes preferidos.

Com poucas linhas no Program.cs, a aplicação passa a registrar eventos estruturados no console, em arquivos e por e-mail. Além disso, a configuração via appsettings.json permite ajustar tudo sem novo deploy.

O próximo passo é escolher os sinks que fazem sentido para o seu time e definir níveis adequados para cada um. Assim, o Serilog entrega informação útil sem afogar ninguém em ruído.

FAQ

O Serilog substitui o ILogger do ASP.NET?

Não, ele se pluga como provedor por trás do ILogger. Seu código continua usando a abstração padrão, e o Serilog cuida de entregar os eventos aos sinks.

Por que minhas mensagens de Debug não aparecem no console?

O nível mínimo padrão do Serilog é Information. Adicione MinimumLevel.Debug() na configuração para liberar as mensagens de Debug.

Preciso instalar um pacote para gravar logs em arquivo?

Não, o pacote Serilog.AspNetCore já inclui o sink de arquivo como dependência. Outros destinos, como e-mail ou Discord, exigem pacotes próprios.

Posso enviar logs para vários destinos ao mesmo tempo?

Sim, basta encadear várias chamadas WriteTo. Cada sink pode ter o seu próprio nível mínimo.

O que é log estruturado?

É o registro que guarda a mensagem e os valores dela como propriedades separadas. Isso permite filtrar e pesquisar eventos por campo em ferramentas de observabilidade.