Serilog no .NET: Logs Estruturados para APIs e Workers
Introdução
Aplicações modernas precisam de logs consistentes, pesquisáveis e fáceis de correlacionar, principalmente em APIs, Workers e ambientes distribuídos. Nesse contexto, o Serilog se destaca no ecossistema .NET por oferecer logs estruturados, integração com múltiplos destinos e uma configuração flexível baseada em sinks, enrichers e filtros. Este artigo apresenta o uso do Serilog em aplicações ASP.NET Core e serviços do tipo Worker, mostrando como a abordagem estruturada melhora a observabilidade, o diagnóstico de falhas e o rastreamento de eventos em produção (CHUVAKIN; SCHMIDT; PHILLIPS, 2012); (MAJORS; FONG-JONES; MIRANDA, 2022).
Fundamentos de Logs Estruturados
Definição e Benefícios
Logs estruturados são uma evolução do log textual tradicional. Em vez de registrar mensagens como texto solto, eles armazenam cada evento com um template, campos nomeados e valores tipados. Isso facilita buscas, filtros, agregações e a correlação entre eventos de diferentes serviços, sem depender de expressões regulares frágeis (CHUVAKIN; SCHMIDT; PHILLIPS, 2012).
Entre os benefícios mais importantes estão a consulta por propriedades, a geração de métricas a partir dos próprios eventos, a correlação com trace ids e correlation ids e a redução do tempo gasto em investigações operacionais. Em ambientes maiores, isso melhora tanto a observabilidade quanto a governança dos dados registrados (MAJORS; FONG-JONES; MIRANDA, 2022); (BEYER et al., 2016).
Contextualização no .NET
No .NET, o logging já faz parte da infraestrutura da plataforma por meio da abstração ILogger<T> e da injeção de dependências. O Serilog amplia esse modelo ao permitir que APIs e Workers registrem eventos com mais contexto, incluindo dados da requisição, usuário autenticado, ambiente de execução e identificadores de correlação.
Arquitetura e Funcionamento do Serilog
Pipeline de Logging do Serilog
O pipeline do Serilog gira em torno de três elementos: Logger, Sink e Enricher. O Logger recebe o evento, os Enrichers adicionam contexto e os Sinks enviam o resultado para destinos como console, arquivo, banco de dados, Seq ou Elasticsearch. Essa separação torna a configuração mais clara e permite adaptar o fluxo de logs às necessidades do sistema (CHUVAKIN; SCHMIDT; PHILLIPS, 2012).
Exemplo de Configuração Multi-Sink
var logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("Application", "MyApiService")
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.File("logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
shared: true)
.WriteTo.Seq("http://localhost:5341")
.CreateLogger();
Integração com APIs ASP.NET Core
Middleware de Logging
A integração do Serilog com o pipeline HTTP ajuda a observar requisições e respostas de forma padronizada. Com UseSerilogRequestLogging, fica mais simples registrar latência, código de status, exceções e informações do usuário ou da chamada, o que melhora a análise de falhas em APIs REST, gRPC e Minimal APIs.
Exemplo de Middleware Utilizando Serilog
public class SerilogRequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SerilogRequestLoggingMiddleware> _logger;
public SerilogRequestLoggingMiddleware(RequestDelegate next, ILogger<SerilogRequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext httpContext)
{
var correlationId = httpContext.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString()))
{
var sw = Stopwatch.StartNew();
try
{
await _next(httpContext);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception on {Method} {Path}",
httpContext.Request.Method, httpContext.Request.Path);
throw;
}
finally
{
sw.Stop();
_logger.LogInformation(
"HTTP {Method} {Path} responded {StatusCode} in {ElapsedMs:0.0000} ms, User: {User}",
httpContext.Request.Method,
httpContext.Request.Path,
httpContext.Response.StatusCode,
sw.Elapsed.TotalMilliseconds,
httpContext.User?.Identity?.Name ?? "anonymous");
}
}
}
}
Boas Práticas na Estruturação dos Logs de APIs
- Utilizar propriedades contextuais como request id, usuário autenticado, tenant e IP de origem.
- Registrar tanto eventos de início e término de request, quanto exceções não tratadas, com stack traces estruturados.
- Adotar níveis de severidade apropriados (
Informationpara fluxos normais,Warningpara condições recuperáveis eError/Criticalpara falhas). - Propagar correlação via cabeçalho X-Correlation-ID para rastreamento entre microserviços, integrando-o ao
Activity.Currentdo .NET (SIGELMAN et al., 2010). - Evitar logar payloads completos sem mascaramento — preferir schemas reduzidos e seguros.
Aplicando Serilog em Worker Services .NET
Contextualização dos Serviços Worker
Serviços do tipo Worker, normalmente implementados com IHostedService ou BackgroundService, executam tarefas assíncronas, consumo de filas e rotinas agendadas. Nesse cenário, logs estruturados são ainda mais importantes porque o diagnóstico depende quase sempre do que foi registrado pelo sistema. Por isso, vale registrar início, fim, duração, falhas, tentativas de repetição e contexto de negócio em cada processamento (MAJORS; FONG-JONES; MIRANDA, 2022).
Exemplo Completo com Background Worker e Logs de Eventos de Negócio
public class QueueProcessorService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<QueueProcessorService> _logger;
public QueueProcessorService(IServiceProvider serviceProvider, ILogger<QueueProcessorService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queue Processor Service started at {StartTime}", DateTime.UtcNow);
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _serviceProvider.CreateScope();
var queue = scope.ServiceProvider.GetRequiredService<IMessageQueue>();
var message = await queue.DequeueMessageAsync(stoppingToken);
if (message is null)
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
continue;
}
var correlationId = message.CorrelationId ?? Guid.NewGuid().ToString();
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("MessageId", message.Id))
{
var sw = Stopwatch.StartNew();
_logger.LogInformation("Processing message {MessageId}", message.Id);
// ... regras de negócio
sw.Stop();
_logger.LogInformation(
"Message {MessageId} processed in {ElapsedMs:0.00} ms",
message.Id, sw.Elapsed.TotalMilliseconds);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Queue Processor Service stopping gracefully");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing message at {Time}", DateTime.UtcNow);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
}
Enrichers e Customização de Contexto
Enriquecendo Logs com Propriedades Dinâmicas
O Serilog permite enriquecer cada evento com propriedades adicionais de forma automática. Entre os dados mais úteis estão nome da máquina, ambiente, versão da aplicação, tenant, identificador transacional e métricas de execução. Isso evita repetição no código e mantém o contexto dos logs mais consistente (CHUVAKIN; SCHMIDT; PHILLIPS, 2012).
Criação de Enricher Customizado: Capturando Nome do Host
public class HostNameEnricher : ILogEventEnricher
{
private readonly LogEventProperty _property;
public HostNameEnricher()
{
_property = new LogEventProperty("HostName", new ScalarValue(Environment.MachineName));
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddPropertyIfAbsent(_property);
}
}
// Uso na configuração:
var logger = new LoggerConfiguration()
.Enrich.With<HostNameEnricher>()
.WriteTo.Console()
.CreateLogger();
Captura do Correlation ID por Enricher
Uma boa prática é definir um correlation id por requisição ou por mensagem processada. Em APIs, isso costuma ser feito no middleware; em Workers, no contexto local do item consumido. Com esse identificador presente em todos os sinks, o rastreamento entre serviços se torna muito mais simples (SIGELMAN et al., 2010).
Persistência de Logs: Arquivo, Banco e Search Engines
Persistência em Arquivos com Rolling Logs
Ao persistir logs em arquivo, é importante definir rotação, limite de tamanho e retenção. Esses parâmetros evitam crescimento descontrolado do volume de dados e mantêm o histórico operacional disponível por tempo suficiente para análise.
.WriteTo.File("logs/api-events-.log",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 10_485_760, // 10 MB
rollOnFileSizeLimit: true,
retainedFileCountLimit: 30,
buffered: true,
flushToDiskInterval: TimeSpan.FromSeconds(2))
Integração com SQL Server e Consultas Avançadas
Armazenar logs em bancos relacionais pode ser útil quando há necessidade de auditoria, relatórios operacionais ou integração com ferramentas de BI. No caso do SQL Server, o sink permite mapear propriedades importantes para colunas específicas e manter a consulta dos eventos mais organizada.
.WriteTo.MSSqlServer(
connectionString: Configuration.GetConnectionString("LogsDb"),
sinkOptions: new MSSqlServerSinkOptions
{
TableName = "AppLogs",
AutoCreateSqlTable = true,
BatchPostingLimit = 100,
BatchPeriod = TimeSpan.FromSeconds(5)
},
columnOptions: new ColumnOptions
{
AdditionalColumns = new List<SqlColumn>
{
new SqlColumn { ColumnName = "Application", DataType = SqlDbType.NVarChar, DataLength = 100 },
new SqlColumn { ColumnName = "RequestId", DataType = SqlDbType.UniqueIdentifier },
new SqlColumn { ColumnName = "Severity", DataType = SqlDbType.NVarChar, DataLength = 20 }
}
})
Elasticsearch, Grafana e Kibana: Observabilidade Contemporânea
Integrar logs estruturados à stack Elastic é uma prática comum em ambientes distribuídos. Com isso, os eventos passam a ser consultáveis em tempo real, permitindo a criação de painéis, buscas por propriedades e alertas automáticos com base em padrões operacionais (MAJORS; FONG-JONES; MIRANDA, 2022).
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
{
AutoRegisterTemplate = true,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
IndexFormat = "apilogs-{0:yyyy.MM.dd}",
CustomFormatter = new ExceptionAsObjectJsonFormatter(),
NumberOfShards = 2,
NumberOfReplicas = 1,
EmitEventFailure = EmitEventFailureHandling.WriteToSelfLog
| EmitEventFailureHandling.RaiseCallback,
FailureCallback = e => Console.WriteLine($"Unable to submit log: {e.MessageTemplate}")
})
Logs e Observabilidade em Sistemas Distribuídos
Tracing, Métricas e Correlação
Em arquiteturas distribuídas, somente logging não basta. O ideal é combinar logs, métricas e traces, de forma que um incidente possa ser analisado por diferentes perspectivas. Quando os logs incluem identificadores de rastreamento, fica mais fácil reconstruir o caminho de uma requisição entre vários serviços (MAJORS; FONG-JONES; MIRANDA, 2022).
Resiliência na Transmissão de Logs
A confiabilidade do pipeline também depende do modo como os sinks entregam os eventos. Buffers, escrita assíncrona, retry e retenção local ajudam a reduzir perdas quando um destino remoto fica indisponível. Em produção, esse cuidado é parte da própria estratégia de confiabilidade do sistema (BEYER et al., 2016).
Segurança e Compliance em Logging
Cuidados com Dados Sensíveis
Logs podem expor dados sensíveis se não houver cuidado na modelagem dos eventos. Senhas, tokens, documentos pessoais e dados financeiros devem ser mascarados ou removidos antes da persistência. No Serilog, isso pode ser tratado com filtros e políticas de destructuring, reduzindo riscos de segurança e problemas regulatórios (CHUVAKIN; SCHMIDT; PHILLIPS, 2012).
Filtro de Propriedades Sensíveis via Serilog
public class SensitiveDataDestructuringPolicy : IDestructuringPolicy
{
private static readonly HashSet<string> SensitiveFields = new(StringComparer.OrdinalIgnoreCase)
{
"Password", "Token", "Authorization", "CreditCard", "Cpf", "Ssn"
};
public bool TryDestructure(object value, ILogEventPropertyValueFactory factory, out LogEventPropertyValue result)
{
if (value is null) { result = null; return false; }
var properties = value.GetType().GetProperties()
.Select(p => new LogEventProperty(
p.Name,
SensitiveFields.Contains(p.Name)
? new ScalarValue("***")
: factory.CreatePropertyValue(p.GetValue(value), true)));
result = new StructureValue(properties);
return true;
}
}
// Configuração:
var logger = new LoggerConfiguration()
.Destructure.With<SensitiveDataDestructuringPolicy>()
.WriteTo.Console()
.CreateLogger();
Compliance, Auditoria e LGPD
Infraestruturas de logging precisam considerar controle de acesso, anonimização, retenção e descarte dos registros. Em termos práticos, isso significa equilibrar rastreabilidade e requisitos legais, como os previstos pela LGPD, sem transformar os logs em uma fonte de exposição desnecessária de dados.
Testando e Validando Estratégias de Logging
Testes Automatizados para Logs
Também é possível testar logging. Em cenários críticos, vale verificar se o sistema está emitindo os eventos esperados, com o nível correto e com as propriedades mais importantes preenchidas. Isso ajuda a evitar observabilidade incompleta em produção.
using (TestCorrelator.CreateContext())
{
var logger = new LoggerConfiguration()
.WriteTo.TestCorrelator()
.CreateLogger();
logger.Information("Testing event with Id {EventId}", 42);
var logEvents = TestCorrelator.GetLogEventsFromCurrentContext().ToList();
Assert.Contains(logEvents, e =>
e.MessageTemplate.Text.Contains("Testing event") &&
e.Properties["EventId"].ToString() == "42");
}
Monitoramento, Diagnóstico de Falhas e Alertas
Dashboards e Alertas Dinâmicos
Quando os logs são estruturados corretamente, ferramentas como Kibana, Grafana e Application Insights conseguem transformar eventos em painéis e alertas úteis. Isso permite detectar aumento de erros, lentidão e comportamentos fora do padrão com mais antecedência (MAJORS; FONG-JONES; MIRANDA, 2022).
Caso Real com Alertas no Elastic/Kibana
// Exemplo: alerta para número anormal de exceptions em 15 minutos
PUT _watcher/watch/exception_alert
{
"trigger": { "schedule": { "interval": "15m" } },
"input": {
"search": {
"request": {
"indices": [ "apilogs-*" ],
"body": {
"query": { "match": { "Level": "Error" } },
"size": 0,
"aggs": {
"errors_count": { "value_count": { "field": "Exception" } }
}
}
}
}
},
"condition": {
"compare": {
"ctx.payload.aggregations.errors_count.value": { "gt": 100 }
}
},
"actions": {
"notify-slack": {
"webhook": {
"method": "POST",
"url": "https://hooks.slack.com/services/xxxx/yyyy",
"body": "{\"text\": \"Number of exceptions in 15min exceeded 100!\"}"
}
}
}
}
Integração com OpenTelemetry e Distributed Tracing
Propagando Traces e Correlation IDs
A integração com OpenTelemetry aproxima logs, métricas e traces no mesmo fluxo de observabilidade. Ao enriquecer os eventos com TraceId e SpanId, a aplicação passa a oferecer uma visão mais completa do caminho percorrido por cada requisição entre serviços (MAJORS; FONG-JONES; MIRANDA, 2022).
Exemplo de Logging Avançado com Enrichment de Trace Info
public class OpenTelemetryEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var span = System.Diagnostics.Activity.Current;
if (span is null) return;
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TraceId", span.TraceId.ToString()));
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("SpanId", span.SpanId.ToString()));
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ParentSpanId", span.ParentSpanId.ToString()));
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("OperationName", span.OperationName));
}
}
// Configuração combinando Serilog + OpenTelemetry
var logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With<OpenTelemetryEnricher>()
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] [{TraceId}/{SpanId}] {Message:lj}{NewLine}{Exception}")
.WriteTo.OpenTelemetry(options =>
{
options.Endpoint = "http://otel-collector:4317";
options.Protocol = OtlpProtocol.Grpc;
options.ResourceAttributes = new Dictionary<string, object>
{
["service.name"] = "MyApiService",
["service.version"] = "1.0.0"
};
})
.CreateLogger();
Considerações Finais
O Serilog é uma escolha sólida para tornar aplicações .NET mais observáveis. Com logs estruturados, enriquecimento contextual, persistência adequada e integração com tracing, APIs e Workers ganham mais previsibilidade operacional. Na prática, isso significa investigar incidentes com mais rapidez, reduzir pontos cegos e transformar o logging em uma parte real da estratégia de confiabilidade do sistema.
Referências
-
CHUVAKIN, Anton; SCHMIDT, Kevin; PHILLIPS, Chris. Logging and log management: the authoritative guide to understanding the concepts surrounding logging and log management. Newnes, 2012.
-
MAJORS, Charity; FONG-JONES, Liz; MIRANDA, George. Observability engineering. " O'Reilly Media, Inc.", 2022.
-
BEYER, Betsy et al. Site reliability engineering: how Google runs production systems. " O'Reilly Media, Inc.", 2016.
-
SIGELMAN, Benjamin H. et al. Dapper, a large-scale distributed systems tracing infrastructure. 2010.