Serilog & Structured Logging 🟡
Serilog es el estándar de facto para logging en .NET. La diferencia clave frente al ILogger básico es el structured logging: en lugar de texto plano, los logs son objetos con propiedades consultables. En producción, esto es lo que permite buscar, filtrar y correlacionar logs de forma efectiva.
Logging de texto vs structured logging
// ❌ Log de texto plano — solo es un string
_logger.LogInformation($"Pedido {pedidoId} creado por usuario {userId} por ${monto}");
// Salida: "Pedido 1234 creado por usuario 99 por $150"
// Para buscar todos los pedidos del usuario 99 → necesitas regex sobre texto
// ✅ Structured log — propiedades indexables
_logger.LogInformation(
"Pedido {PedidoId} creado por usuario {UserId} por {Monto:C}",
pedidoId, userId, monto);
// Salida JSON: { "PedidoId": 1234, "UserId": 99, "Monto": 150.0, "Message": "..." }
// Para buscar todos los pedidos del usuario 99 → query: UserId = 99
La diferencia en producción:
- Texto plano: cuando hay un incidente, buscas con grep/regex en megas de logs
- Structured: buscas
PedidoId = 1234 AND Level = Erroren Seq, Elastic o Application Insights
Setup de Serilog en .NET 8
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq # Dashboard local de logs
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process
// Program.cs — configuración completa
using Serilog;
using Serilog.Events;
// Configurar Serilog ANTES de construir el host
// Así captura errores del propio startup
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext() // Permite agregar propiedades dinámicas
.Enrich.WithEnvironmentName() // Agrega "EnvironmentName" al log
.Enrich.WithMachineName() // Agrega "MachineName"
.Enrich.WithThreadId() // Agrega "ThreadId"
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.File(
path: "logs/app-.log",
rollingInterval: RollingInterval.Day, // Un archivo por día
retainedFileCountLimit: 7, // Guardar 7 días
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.Seq("http://localhost:5341") // Dashboard de logs (desarrollo)
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
// Reemplazar el logger de .NET con Serilog
builder.Host.UseSerilog();
var app = builder.Build();
// Log de cada request HTTP (reemplaza el middleware de logging de .NET)
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} respondió {StatusCode} en {Elapsed:0.0000}ms";
// Agregar propiedades adicionales al log del request
options.EnricherForRequest = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"]);
diagnosticContext.Set("UserId", httpContext.User.FindFirst("sub")?.Value);
};
});
try
{
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "La aplicación terminó de forma inesperada");
}
finally
{
Log.CloseAndFlush(); // Importante: vaciar el buffer antes de cerrar
}
Configuración por appsettings.json
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {CorrelationId} {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/app-.log",
"rollingInterval": "Day"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
}
}
// Leer config de appsettings.json
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.CreateLogger();
LogContext — agregar propiedades dinámicas
// ✅ Agregar propiedades a todos los logs dentro de un bloque
using (LogContext.PushProperty("PedidoId", pedidoId))
using (LogContext.PushProperty("UserId", userId))
{
_logger.LogInformation("Iniciando procesamiento");
await ValidarPedidoAsync();
await ProcesarPagoAsync();
_logger.LogInformation("Procesamiento completado");
// Todos estos logs tienen PedidoId y UserId como propiedades
}
// ✅ En un middleware — agregar CorrelationId a TODOS los logs del request
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// Obtener o generar CorrelationId
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString("N")[..8];
// Agregar al response header
context.Response.Headers["X-Correlation-Id"] = correlationId;
// Agregar a TODOS los logs del request mediante LogContext
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await _next(context);
}
}
}
// Registrar en Program.cs (ANTES de UseSerilogRequestLogging)
app.UseMiddleware<CorrelationIdMiddleware>();
Niveles de log — cuándo usar cada uno
// Verbose — máximo detalle, solo en debugging profundo
_logger.LogTrace("Entrando a ValidarPedido con dto: {@Dto}", dto);
// Debug — información útil en desarrollo, off en producción
_logger.LogDebug("Cache miss para key {CacheKey}", cacheKey);
// Information — flujo normal de la app, lo que quieres ver en prod
_logger.LogInformation("Pedido {PedidoId} creado exitosamente", pedidoId);
// Warning — algo inesperado pero la app sigue funcionando
_logger.LogWarning("Reintento {Attempt}/3 para servicio externo {Servicio}", attempt, servicio);
// Error — falló una operación, requiere atención
_logger.LogError(ex, "Error procesando pedido {PedidoId}", pedidoId);
// Fatal / Critical — la app está a punto de caer o ya cayó
_logger.LogCritical(ex, "No se pudo conectar a la base de datos al iniciar");
Configuración típica por ambiente:
Desarrollo: Verbose o Debug — ver todo
Staging: Information — solo flujo normal + warnings
Producción: Warning — solo lo que requiere atención
(Exception: tracing selectivo con Sampling)
Destructuring — loguear objetos
// @ — destructura el objeto en propiedades individuales
_logger.LogInformation("Pedido creado: {@Pedido}", pedido);
// Salida: { "Pedido": { "Id": 1, "Monto": 150, "Estado": "Pendiente" } }
// $ — serializa como string (toString)
_logger.LogInformation("Pedido creado: {$Pedido}", pedido);
// Salida: { "Pedido": "MiApp.Models.Pedido" } — menos útil
// Sin prefijo — tipo primitivo o string
_logger.LogInformation("Usuario {UserId} logueado", userId);
// ⚠️ Cuidado con datos sensibles
_logger.LogInformation("Pago procesado: {@Tarjeta}", tarjeta);
// ❌ Puede loguear número de tarjeta y CVV
// ✅ Usar destructuring con control o anonimizar
_logger.LogInformation("Pago procesado para tarjeta terminada en {UltimosDigitos}",
tarjeta.Numero[^4..]);
Sinks — dónde van los logs
// ✅ Seq — dashboard web local, ideal para desarrollo
// dotnet add package Serilog.Sinks.Seq
.WriteTo.Seq("http://localhost:5341")
// docker run -e ACCEPT_EULA=Y -p 5341:80 datalust/seq
// ✅ Application Insights — Azure
// dotnet add package Serilog.Sinks.ApplicationInsights
.WriteTo.ApplicationInsights(
TelemetryConfiguration.Active,
TelemetryConverter.Traces)
// ✅ Elasticsearch
// dotnet add package Serilog.Sinks.Elasticsearch
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
{
IndexFormat = "mi-app-{0:yyyy.MM.dd}",
AutoRegisterTemplate = true
})
// ✅ Async sink — no bloquear el thread principal al escribir logs
// dotnet add package Serilog.Sinks.Async
.WriteTo.Async(a => a.File("logs/app-.log", rollingInterval: RollingInterval.Day))
Correlation ID en microservicios
En sistemas distribuidos, el Correlation ID permite rastrear un request a través de múltiples servicios:
// ✅ Propagar CorrelationId entre servicios via HttpClient
public class CorrelationIdHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CorrelationIdHandler(IHttpContextAccessor accessor)
{
_httpContextAccessor = accessor;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Leer CorrelationId del request actual y pasarlo al servicio externo
var correlationId = _httpContextAccessor.HttpContext?
.Response.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString("N")[..8];
request.Headers.Add("X-Correlation-Id", correlationId);
return await base.SendAsync(request, cancellationToken);
}
}
// Registrar como handler de HttpClient
builder.Services.AddTransient<CorrelationIdHandler>();
builder.Services.AddHttpClient<IInventarioClient, InventarioClient>()
.AddHttpMessageHandler<CorrelationIdHandler>();
Con CorrelationId propagado:
Request usuario → API Gateway (CorrelationId: abc123)
→ Orders Service (log: CorrelationId=abc123)
→ Inventory Service (log: CorrelationId=abc123)
→ Payment Service (log: CorrelationId=abc123)
Cuando falla el pago:
Buscar CorrelationId=abc123 → ves el journey completo del request
en todos los servicios, en orden cronológico
Preguntas frecuentes de entrevista 🎯
1. ¿Cuál es la diferencia entre logging de texto y structured logging?
En texto plano, el log es un string: "Pedido 123 creado por usuario 99". En structured logging, el log es un objeto con propiedades:
{ PedidoId: 123, UserId: 99 }. La diferencia en producción es enorme: con structured logging puedes hacer queries comoPedidoId = 123 AND Level = Erroren Seq o Elastic en milisegundos. Con texto plano necesitas grep sobre gigabytes de archivos.
2. ¿Por qué usas {PedidoId} en lugar de interpolación $"{pedidoId}"?
Con interpolación (
$"Pedido {pedidoId}") el mensaje ya es un string — Serilog no puede extraer la propiedadPedidoIdpor separado. Con el template ("Pedido {PedidoId}") Serilog captura el valor como propiedad tipada en el LogEvent, además del mensaje formateado. La propiedad queda consultable en el sink (Seq, Elastic, etc.).
3. ¿Qué es LogContext y para qué lo usas?
LogContext.PushPropertyagrega una propiedad a todos los logs que se emitan dentro de ese scope usando un ambient context (AsyncLocal). Es el mecanismo para que propiedades comoCorrelationId,UserIdoPedidoIdaparezcan en todos los logs de un request sin pasarlas manualmente en cada llamada.
4. ¿Cómo evitas loguear datos sensibles?
Evitando
{@Objeto}en entidades que contengan datos sensibles (tarjetas, contraseñas, tokens). En su lugar, loguear solo los campos necesarios:tarjeta.Numero[^4..]en vez de{@Tarjeta}. Serilog tiene un mecanismo de destructuring policies que puede sanitizar automáticamente propiedades por nombre o tipo.
5. ¿Qué es un Correlation ID y por qué es crítico en microservicios?
Es un identificador único (GUID o short hash) que se genera en el primer servicio que recibe el request y se propaga en los headers HTTP a todos los servicios downstream. Permite reconstruir el journey completo de un request a través de múltiples servicios cuando hay un error. Sin él, en un incidente tendrías logs de 5 servicios sin saber cuáles pertenecen al mismo request.