Leaders Logo

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 (Information para fluxos normais, Warning para condições recuperáveis e Error/Critical para falhas).
  • Propagar correlação via cabeçalho X-Correlation-ID para rastreamento entre microserviços, integrando-o ao Activity.Current do .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. reference.Description
  • MAJORS, Charity; FONG-JONES, Liz; MIRANDA, George. Observability engineering. " O'Reilly Media, Inc.", 2022. reference.Description
  • BEYER, Betsy et al. Site reliability engineering: how Google runs production systems. " O'Reilly Media, Inc.", 2016. reference.Description
  • SIGELMAN, Benjamin H. et al. Dapper, a large-scale distributed systems tracing infrastructure. 2010. reference.Description
Sobre o autor