Logging en .NET: qué registrar y qué no
Logs que no sirven para diagnosticar, que cuestan más que el servidor o que guardan contraseñas. Qué registrar en .NET, con qué nivel y en qué formato.
Hay un incidente en producción. Abres los logs y encuentras dos cosas: millones de líneas de "Entrando al método X" y "Saliendo del método X", y ninguna que diga qué pedido falló ni por qué. O lo contrario: el log tiene todo, incluido el cuerpo completo de cada petición, con números de tarjeta y contraseñas.
Registrar mucho no es lo mismo que registrar bien. Un buen log responde una pregunta concreta: qué pasó, a quién y por qué, cuando algo sale mal.
Logging estructurado, no texto concatenado
// ❌ Una cadena: imposible de filtrar por pedido o por cliente
logger.LogInformation("Pedido " + order.Id + " creado para el cliente " + customerId);
// ✅ Plantilla con propiedades: cada valor queda como un campo
logger.LogInformation("Pedido {OrderId} creado para el cliente {CustomerId}", order.Id, customerId);
Con la segunda forma, el proveedor de logs (Seq, Application Insights, Elastic, Loki) guarda OrderId y CustomerId como campos. Puedes buscar "todos los eventos del pedido 1001" sin expresiones regulares.
La interpolación con $"..." tiene el mismo problema que la concatenación, y además construye la cadena aunque el nivel de log esté desactivado.
LoggerMessage para las rutas con más tráfico
En código que se ejecuta miles de veces por segundo, el source generator de .NET evita asignaciones y la evaluación de parámetros cuando el nivel no está activo:
public static partial class OrderLogs
{
[LoggerMessage(Level = LogLevel.Information, Message = "Pedido {OrderId} creado por {Total} para {CustomerId}")]
public static partial void OrderCreated(this ILogger logger, int orderId, decimal total, int customerId);
[LoggerMessage(Level = LogLevel.Warning, Message = "Pago rechazado para el pedido {OrderId}: {Reason}")]
public static partial void PaymentRejected(this ILogger logger, int orderId, string reason);
}
logger.OrderCreated(order.Id, order.Total, order.CustomerId);
Además de ser más eficiente, deja los mensajes en un solo lugar, con nombres claros.
Niveles: qué va en cada uno
| Nivel | Para qué | Ejemplo |
|---|---|---|
Trace / Debug |
Diagnóstico detallado; apagado en producción | Valores intermedios de un cálculo |
Information |
Eventos de negocio que importan | Pedido creado, pago aprobado, usuario registrado |
Warning |
Algo inesperado que el sistema manejó | Reintento de una API externa, pago rechazado |
Error |
Una operación falló | Excepción no controlada, integración caída |
Critical |
La aplicación no puede seguir funcionando | Sin conexión a la base de datos al arrancar |
El error más común es usar Error para cosas que son esperadas: un usuario que ingresa una contraseña incorrecta no es un error del sistema. Si todo es error, las alertas se vuelven ruido y la gente deja de mirarlas.
Qué registrar
- Eventos de negocio con sus identificadores: pedido, cliente, factura.
- Decisiones del sistema: "el pedido se envió a revisión manual porque el monto supera el límite".
- Llamadas a sistemas externos que fallan o tardan, con duración y código de respuesta.
- Excepciones con el objeto completo (
logger.LogError(ex, ...)), no soloex.Message. - Contexto de la petición: un identificador de correlación que permita unir todas las líneas de una misma operación.
Qué NO registrar
- Datos sensibles: contraseñas, tokens, números de tarjeta, documentos de identidad, datos de salud. En muchos países, incluida Colombia con su ley de protección de datos, un log con datos personales es un repositorio de datos personales con todas sus obligaciones.
- Cuerpos completos de petición y respuesta de forma indiscriminada. Si hace falta para depurar una integración, que sea temporal, filtrado y con campos enmascarados.
- "Entrando / saliendo del método". Para eso existe el tracing distribuido, que lo hace mejor y con duración.
- El mismo error en cada capa. Si el repositorio registra la excepción, el servicio la registra otra vez y el controlador también, un fallo aparece tres veces. Regístralo donde se maneja, no donde pasa.
Scopes: contexto sin repetirlo
using (logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = order.Id,
["TenantId"] = tenantId
}))
{
logger.LogInformation("Validando inventario");
await inventory.ReserveAsync(order, ct);
logger.LogInformation("Inventario reservado");
}
Cada línea dentro del scope lleva OrderId y TenantId sin pasarlos en cada llamada. Revisa que tu proveedor de logs tenga los scopes activados (en la consola, IncludeScopes = true).
Configurar niveles por categoría
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
ASP.NET Core y EF Core son muy habladores en Information. Subir su nivel a Warning en producción reduce mucho el volumen sin perder lo importante. Y como es configuración, puedes bajarlo temporalmente para investigar un problema sin desplegar.
El trade-off
Los logs cuestan: almacenamiento, ingesta (muchos servicios cobran por GB) y algo de rendimiento. Un sistema que registra cada petición con detalle puede pagar más por sus logs que por su servidor. Por otro lado, un log escaso convierte cada incidente en una adivinanza.
El equilibrio razonable: Information para eventos de negocio, Warning y Error con contexto completo, y todo lo demás apagado salvo cuando se investiga algo concreto.
Cuándo los logs no son la herramienta
- Para medir (peticiones por segundo, latencia, errores por minuto): usa métricas. Son mucho más baratas que contar líneas de log.
- Para seguir una petición entre servicios: usa tracing distribuido con OpenTelemetry.
- Para auditoría con valor legal o de negocio (quién cambió qué y cuándo): una tabla de auditoría en la base de datos, no un log que se rota cada 30 días.
Conclusión
Un buen log se escribe pensando en el incidente: ¿qué necesitaría saber a las 3 de la mañana para entender qué falló? Eventos de negocio con identificadores, errores con su excepción, nada sensible y sin ruido. Lo demás son métricas o trazas. Y para que los errores no se pierdan antes de llegar al log, revisa el manejo global de excepciones en ASP.NET Core.
¿Necesitas un equipo .NET para tu proyecto?
Construimos y modernizamos backends en .NET y SQL Server: APIs, integraciones y sistemas empresariales a medida.
Ver desarrollo a medida