Console.WriteLine e _logger.LogInformation("Processando pedido " + orderId) são logging primitivo. Em produção, você precisa de logging estruturado: campos indexáveis, contexto rico, correlação entre serviços e pesquisa eficiente. Serilog é o padrão de facto no ecossistema .NET para isso.
Por que logging estruturado importa
A diferença fundamental:
# Logging textual — impossível pesquisar por campo
[2026-05-02 10:00:00] Pedido 3fa85f64 processado pelo usuário danilo@neryx.com.br em 245ms
# Logging estruturado — cada campo é pesquisável e indexável
{
"timestamp": "2026-05-02T10:00:00.000Z",
"level": "Information",
"message": "Pedido processado",
"orderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"userId": "danilo@neryx.com.br",
"durationMs": 245,
"correlationId": "abc123",
"service": "OrderService",
"environment": "production"
}
Com logging estruturado, você pesquisa no Kibana: level:Error AND service:OrderService AND durationMs:>1000. Com logging textual, você reza para encontrar o erro em gigabytes de texto.
Instalação
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console # Console com formatação
dotnet add package Serilog.Sinks.File # Arquivos com rolagem
dotnet add package Serilog.Sinks.Elasticsearch # ELK Stack
dotnet add package Serilog.Enrichers.Environment # MachineName, EnvironmentName
dotnet add package Serilog.Enrichers.Thread # ThreadId
dotnet add package Serilog.Enrichers.Process # ProcessId, ProcessName
dotnet add package Serilog.Enrichers.Span # TraceId, SpanId (OpenTelemetry)
Configuração completa no Program.cs
// Program.cs — Serilog como primeiro passo (captura erros de startup)
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Warning() // Bootstrap logger mínimo
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, services, configuration) =>
{
configuration
// Nível base por ambiente
.MinimumLevel.Is(context.HostingEnvironment.IsProduction()
? LogEventLevel.Information
: LogEventLevel.Debug)
// Silenciar namespaces verbosos
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command",
context.HostingEnvironment.IsProduction()
? LogEventLevel.Warning // Silencia SQL em produção
: LogEventLevel.Information) // Mostra SQL em desenvolvimento
// Enrichers — enriquecem todo log automaticamente
.Enrich.FromLogContext() // Contexto pushado via LogContext.Push
.Enrich.WithMachineName() // servidor/pod
.Enrich.WithEnvironmentName() // Production/Development/Staging
.Enrich.WithProcessId()
.Enrich.WithThreadId()
.Enrich.WithSpan() // OpenTelemetry TraceId/SpanId
// Propriedades fixas para todos os logs
.Enrich.WithProperty("Application", "OrderService")
.Enrich.WithProperty("Version",
Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown")
// Sinks
.WriteTo.Console(new CompactJsonFormatter()) // JSON compacto no console (para ECS/Datadog)
.WriteTo.File(
new CompactJsonFormatter(),
path: "logs/log-.json",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7,
fileSizeLimitBytes: 100 * 1024 * 1024) // 100MB por arquivo
// Elasticsearch (produção)
.WriteTo.Conditional(
_ => context.HostingEnvironment.IsProduction(),
wt => wt.Elasticsearch(new ElasticsearchSinkOptions(
new Uri(context.Configuration["Serilog:Elasticsearch:Uri"]!))
{
AutoRegisterTemplate = true,
IndexFormat = "orderservice-{0:yyyy.MM.dd}",
NumberOfReplicas = 1,
NumberOfShards = 2,
BatchAction = ElasticOpType.Create
}))
// Lê configuração adicional do appsettings.json
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services); // Injeta serviços nos enrichers
});
// ... resto da configuração
var app = builder.Build();
// Middleware de logging de requisições HTTP (Serilog nativo)
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} respondeu {StatusCode} em {Elapsed:0.0000}ms";
// Enriquecer com dados da requisição e resposta
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent);
diagnosticContext.Set("UserId", httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value);
diagnosticContext.Set("TenantId", httpContext.Items["TenantId"]?.ToString());
diagnosticContext.Set("CorrelationId", httpContext.Items["CorrelationId"]?.ToString());
};
// Ignorar health checks no log de requisições
options.GetLevel = (httpContext, elapsed, ex) =>
{
if (httpContext.Request.Path.StartsWithSegments("/health"))
return LogEventLevel.Verbose; // Verbose = não aparece (abaixo do mínimo)
return ex is not null || httpContext.Response.StatusCode > 499
? LogEventLevel.Error
: LogEventLevel.Information;
};
});
await app.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Aplicação encerrada com falha no startup");
}
finally
{
await Log.CloseAndFlushAsync(); // Garante que todos os logs são escritos antes de encerrar
}
appsettings.json — configuração por ambiente
// appsettings.json (base)
{
"Serilog": {
"Elasticsearch": {
"Uri": "http://elasticsearch:9200"
}
}
}
// appsettings.Development.json — logs detalhados em dev
{
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
}
// appsettings.Production.json — apenas o necessário
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
}
}
}
Logging com contexto estruturado
// ❌ Interpolação de string — não é estruturado, não é indexável
_logger.LogInformation($"Pedido {orderId} do usuário {userId} processado em {duration}ms");
// ✅ Message template — cada propriedade é um campo estruturado
_logger.LogInformation(
"Pedido {OrderId} do usuário {UserId} processado em {DurationMs}ms",
orderId, userId, duration);
// ✅ Com objeto complexo — use @ para serializar como JSON
_logger.LogInformation("Pedido criado: {@Order}", order);
// ✅ Destruturing explícito (serialização controlada)
_logger.LogInformation("Pagamento processado: {Payment}",
new { order.Id, order.Total, order.Status }); // Apenas os campos relevantes
LogContext: enriquecer logs dentro de um escopo
// Serviço que processa pedidos — adiciona contexto para todos os logs dentro do escopo
public class OrderProcessingService
{
private readonly ILogger<OrderProcessingService> _logger;
public async Task ProcessOrderAsync(Guid orderId, Guid tenantId)
{
// Tudo logado dentro deste using terá OrderId e TenantId
using var scope = LogContext.PushProperty("OrderId", orderId);
using var tenantScope = LogContext.PushProperty("TenantId", tenantId);
_logger.LogInformation("Iniciando processamento do pedido");
try
{
await ValidateStockAsync(orderId);
await ProcessPaymentAsync(orderId);
await NotifyCustomerAsync(orderId);
_logger.LogInformation("Pedido processado com sucesso");
}
catch (Exception ex)
{
// Este log terá OrderId e TenantId automaticamente
_logger.LogError(ex, "Falha no processamento do pedido");
throw;
}
}
}
// Extensão para adicionar múltiplas propriedades de uma vez
public static class LogContextExtensions
{
public static IDisposable PushOrderContext(this ILogger logger, Order order)
{
return LogContext.Push(
new PropertyEnricher("OrderId", order.Id),
new PropertyEnricher("OrderStatus", order.Status),
new PropertyEnricher("CustomerId", order.CustomerId),
new PropertyEnricher("OrderTotal", order.Total)
);
}
}
Enricher customizado: dados do tenant
public class TenantEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantEnricher(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var context = _httpContextAccessor.HttpContext;
if (context is null) return;
// Adiciona TenantId se disponível no contexto HTTP
if (context.Items.TryGetValue("TenantId", out var tenantId) && tenantId is not null)
{
logEvent.AddPropertyIfAbsent(
propertyFactory.CreateProperty("TenantId", tenantId.ToString()));
}
// Adiciona CorrelationId
if (context.Items.TryGetValue("CorrelationId", out var correlationId) && correlationId is not null)
{
logEvent.AddPropertyIfAbsent(
propertyFactory.CreateProperty("CorrelationId", correlationId.ToString()));
}
}
}
// Registrar no DI e no Serilog
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<TenantEnricher>();
// No UseSerilog:
.Enrich.With<TenantEnricher>() // Via serviço registrado no DI
Stack ELK com Docker Compose para desenvolvimento
# docker-compose.logging.yml
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m
ports:
- "9200:9200"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
healthcheck:
test: curl -s http://localhost:9200/_cluster/health | grep -v red
interval: 10s
retries: 10
kibana:
image: docker.elastic.co/kibana/kibana:8.13.0
ports:
- "5601:5601"
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
depends_on:
elasticsearch:
condition: service_healthy
volumes:
elasticsearch_data:
# Subir o stack de logging
docker compose -f docker-compose.logging.yml up -d
# Acessar Kibana
open http://localhost:5601
# Criar index pattern: orderservice-* (via Kibana UI ou API)
curl -X POST http://localhost:5601/api/index_patterns/index_pattern \
-H "Content-Type: application/json" \
-H "kbn-xsrf: true" \
-d '{"index_pattern": {"title": "orderservice-*", "timeFieldName": "@timestamp"}}'
Integração com OpenTelemetry
// Serilog + OpenTelemetry: TraceId/SpanId em todos os logs
// Isso correlaciona logs com traces no Jaeger/Grafana Tempo
dotnet add package Serilog.Enrichers.Span
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// Program.cs
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter()); // Exporta para Jaeger/Grafana Tempo
// No Serilog:
.Enrich.WithSpan() // Adiciona TraceId e SpanId de cada span ativo
// Resultado: cada log terá
// "TraceId": "4bf92f3577b34da6a3ce929d0e0e4736",
// "SpanId": "00f067aa0ba902b7"
// Que você pode cruzar com traces no Jaeger!
Checklist de logging para produção
- Nunca interpolação de string — use message templates com propriedades nomeadas
- MinimumLevel por namespace — silenciar Microsoft e System em produção
- Bootstrap logger — captura erros antes do DI estar configurado
- CloseAndFlushAsync no finally — garante que logs não são perdidos no shutdown
- Ignorar health checks — evita poluir logs com 100 requisições/segundo de probes
- CorrelationId em todo log — rastreabilidade entre serviços
- Não logar dados sensíveis — senhas, tokens, CPF, cartão de crédito
- Destruturing controlado —
@para objetos, mas com cuidado no tamanho
Observabilidade é o que separa times que reagem a problemas em produção dos que os antecipam. Se você precisa de uma estratégia de logging e observabilidade para sua stack .NET, fale com a Neryx.