La capacidad de observación y el mentenimiento de una aplicación moderna son aspectos críticos que dependen en gran medida de un sistema de registro de eventos (logging) robusto. Un sistema de logs bien diseñado no solo facilita el monitoreo del comportamiento de la aplicación y la identificación rápida de problemas, sino que también es fundamental para asegurar su estabilidad y confiabilidad a largo plazo. Aunque el ecosistema .NET Core ofrece un proveedor de logging integrado, existen bibliotecas de terceros que expanden significativamente estas capacidades, y Serilog es una de las más destacadas por su potencia y flexibilidad.
Integración de Serilog en Proyectos .NET
Serilog es una biblioteca de loggging muy popular que se caracteriza por su enfoque en el registro estructurado y la facilidad de configuración. Para incorporar Serilog en un proyecto .NET Core, es necesario añadir ciertas dependencias que permiten su interacción con el sistema de logging estándar de Microsoft, la escritura en archivos y el procesamiento asíncrono de los eventos. Las principales bibliotecas a instalar son:
Serilog.Extensions.Logging: Proporciona la capa de compatibilidad para que Serilog funcione como proveedor de la interfazMicrosoft.Extensions.Logging.ILogger.Serilog.Sinks.RollingFile: Un "sink" o destino que permite a Serilog escribir eventos en archivos que se rotan automáticamente basándose en la fecha o el tamaño para gestionar su crecimiento.Serilog.Sinks.Async: Habilita el registro asíncrono, lo cual es vital para el rendimiento, ya que las operaciones de escritura de logs no bloquean los hilos principales de la aplicación.
Definición de Formatos de Registro
Un formato de log claro y conciso es fundamental para la depuración y el análisis de problemas. Serilog permite una personalización profunda del formato de salida de los eventos de log mediante el parámetro outputTemplate. Esto asegura que solo la información relevante se registre, optimizando el espacio y facilitando la interpretación.
A continuación, se muestra cómo configurar Serilog para emitir logs a la consola con un formato específico:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate:
"[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
Los marcadores de posición más comunes para el outputTemplate son:
Timestamp: La fecha y hora exacta en que ocurrió el evento de log.Level: El nivel de severidad del evento, a menudo abreviado (por ejemplo,u3para INFO, WRN, ERR).Message: El contenido del mensaje de log. El formatoljlo renderiza como JSON si es estructurado.Exception: Contiene los detalles completos de una excepción, incluyendo la pila de llamadas.NewLine: Inserta un salto de línea en el mensaje.
Clasificación y Filtrado por Niveles de Severidad
Para categorizar los eventos según su importancia, los logs se clasifican en diferentes niveles de severidad. Estos niveles son cruciales para el filtrado y para decidir qué información es relevante en diferentes entornos de ejecución:
- Debug: Información muy detallada, útil para el seguimiento durante el desarrollo o la depuración de problemas específicos.
- Information: Mensajes que ilustran el flujo normal de la aplicación, como el inicio de un servicio o la finalización de una tarea.
- Warning: Indican situaciones que no son errores críticos pero que podrían derivar en problemas futuros o merecen atención.
- Error: Registran fallos en operaciones que impiden la ejecución normal de una parte de la aplicación, pero no detienen la aplicación completa.
- Fatal: Errores extremadamente graves que indican un fallo irreparable y que usualmente llevan al cierre de la aplicación.
Serilog ofrece la capacidad de enrutar logs de diferentes niveles a distintos "sinks" o destinos. Esta funcionalidad es invaluable para mantener los logs organizados; por ejemplo, enviando los mensajes de depuración a un archivo separado de los errores críticos.
Un ejemplo de cómo filtrar logs para dirigir solo los eventos de nivel Debug a un archivo específico:
var configuracionBase = new LoggerConfiguration()
.MinimumLevel.Debug() // Establece el nivel mínimo global que se procesará
.WriteTo.Logger(filtroNivel => filtroNivel
.Filter.ByIncludingOnly(evento => evento.Level == LogEventLevel.Debug)
.WriteTo.RollingFile("logs/app-depuracion-{Date}.txt"));
// Para usar esta configuración, se llamaría a .CreateLogger() en ella.
Ejemplo de Configuración Detallada en .NET Core (MVC/Web API)
Para una integración completa de Serilog en una aplicación .NET Core, la configuración se realiza típicamente en el archivo Startup.cs (o Program.cs para .NET 6+). El siguiente código ilustra cómo configurar Serilog para gestionar logs de múltiples niveles, escribiéndolos asíncronamente en archivos rotativos específicos para cada nivel:
// En el método ConfigureServices de Startup.cs o directamente en Program.cs
public void ConfigureServices(IServiceCollection services)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() // Nivel mínimo de logs que Serilog procesará
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) // Reduce el detalle de logs de los componentes de Microsoft
.MinimumLevel.Override("System", LogEventLevel.Information) // Reduce el detalle de logs de los componentes del sistema
.Enrich.FromLogContext() // Permite añadir propiedades de contexto a los logs
// Sink para logs de DEBUG
.WriteTo.Logger(configDebug => configDebug
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Debug)
.WriteTo.Async(asyncSink => asyncSink.RollingFile("logs/app-debug-{Date}.txt")))
// Sink para logs de INFORMATION
.WriteTo.Logger(configInfo => configInfo
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Information)
.WriteTo.Async(asyncSink => asyncSink.RollingFile("logs/app-informacion-{Date}.txt")))
// Sink para logs de WARNING
.WriteTo.Logger(configWarn => configWarn
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Warning)
.WriteTo.Async(asyncSink => asyncSink.RollingFile("logs/app-advertencia-{Date}.txt")))
// Sink para logs de ERROR
.WriteTo.Logger(configError => configError
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Error)
.WriteTo.Async(asyncSink => asyncSink.RollingFile("logs/app-error-{Date}.txt")))
// Sink para logs FATAL
.WriteTo.Logger(configFatal => configFatal
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Fatal)
.WriteTo.Async(asyncSink => asyncSink.RollingFile("logs/app-fatal-{Date}.txt")))
.CreateLogger();
// En Startup.cs, si no se usa UseSerilog() del paquete Serilog.AspNetCore.
// services.AddLogging(builder => builder.AddSerilog(dispose: true));
}
// En el método Configure de Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
// Esto integra Serilog con la abstracción de ILogger de .NET Core,
// permitiendo que las inyecciones de ILogger utilicen Serilog.
loggerFactory.AddSerilog();
// Configuraciones adicionales del pipeline HTTP de la aplicación
// app.UseHttpsRedirection();
// app.UseRouting();
// app.UseAuthorization();
// app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
Una vez que Serilog está configurado, cualquier instancia de ILogger<T> inyectada en sus clases utilizará automáticamente Serilog como su proveedor de logging. También es posible usar el objeto logger estático global de Serilog, Log.Logger, directamente.
A continuación, un ejemplo de cómo registrar un evento de error en un controlador de ASP.NET Core, utilizando tanto la inyección de dependencia de ILogger como el logger estático de Serilog:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; // Para ILogger
using Serilog; // Para el logger estático Log.Logger
using System;
public class PedidosController : Controller
{
private readonly ILogger<PedidosController> _registrador; // Inyección de ILogger
// Constructor que inyecta ILogger
public PedidosController(ILogger<PedidosController> registrador)
{
_registrador = registrador;
}
public IActionResult ProcesarPedido(int idPedido)
{
try
{
// Simulación de una operación que podría fallar
if (idPedido <= 0)
{
throw new ArgumentException("El ID del pedido no es válido.");
}
// Lógica para procesar el pedido...
_registrador.LogInformation("Pedido {PedidoId} procesado exitosamente.", idPedido);
}
catch (Exception ex)
{
// Utilizando el ILogger inyectado para registrar el error
_registrador.LogError(ex, "Fallo al procesar el pedido con ID: {PedidoId}.", idPedido);
// También se podría usar el logger estático de Serilog directamente para errores críticos
Log.Fatal(ex, "Excepción crítica detectada en el procesamiento de pedidos para ID: {PedidoId}.", idPedido);
}
return View("Confirmacion");
}
}
Al ejecutar la aplicación y disparar una condición de error, por ejemplo, visitando una ruta que ejecuta ProcesarPedido con un ID no válido, Serilog generará los archivos de log correspondientes en la carpeta logs/, como app-error-{Date}.txt y app-fatal-{Date}.txt, con los detalles de las excepciones registradas.