Dinaup.Logs
Módulo de observabilidad de Dinaup para .NET, basado en Serilog: logs estructurados, contexto, correlación distribuida, medición de tiempos y envío a Loki.
Dinaup.Logs es el módulo de observabilidad de la librería Dinaup. Está construido sobre Serilog y unifica logs, contexto y medición de tiempos en tus aplicaciones .NET. Incluye:
- Logs estructurados con contexto (Component, Action).
- Correlación distribuida (CorrelationId) para rastrear un flujo entre servicios.
- Medición de duración de operaciones (
BeginMeasure) con umbral configurable. - Envío opcional y centralizado a Grafana Loki y, si lo activas, notificaciones push por ntfy.
- Helpers para envolver código con medición de tiempo, manejo de errores y logs coherentes.
Requisitos
Dinaup .NET
Antes de empezar, instala el paquete Dinaup.
https://www.nuget.org/packages/Dinaup
Grafana Loki (opcional)
Por defecto los logs se escriben a archivo y consola, sin infraestructura adicional. Para centralizar y consultar tus logs recomendamos enviarlos a Grafana Loki (autoalojado o Grafana Cloud).
- Solo necesitas el endpoint de push de Loki y, si tu instancia lo requiere, usuario y contraseña (basic auth).
- La activación es opcional: si no pasas
LokiConfig, el log sigue funcionando en local.
Inicio
Logs es una clase estática: no se instancia ni necesita una sesión ni el cliente de la API. Llama a Initialize una vez al arrancar la aplicación.
using Dinaup;
using Serilog.Events;
// Inicialización mínima: escribe a logs\log.txt y consola.
// El nombre y la versión de la app se detectan del ensamblado de entrada.
Logs.Initialize();
Logs.SetLoggingLevel(LogEventLevel.Information);using Dinaup;
using Serilog.Events;
// (Opcional) Promueve propiedades extra a labels de Loki.
// IMPORTANTE: configúralas ANTES de Initialize.
Logs.propertiesAsLabels.Add("Environment");
var loki = new Logs.LokiConfig
{
Endpoint = "https://loki.tu-dominio.com/loki/api/v1/push",
Username = "usuario", // opcional (basic auth)
Password = "password" // opcional
};
Logs.Initialize(loki);
Logs.SetLoggingLevel(LogEventLevel.Information);Las labels base siempre presentes en Loki son service_name, MachineName, Version y Environment. Con Logs.propertiesAsLabels añades labels propias a partir de las propiedades estructuradas de tus logs.
Añade solo labels de baja cardinalidad (región, entorno, tier). Nunca UserId, CorrelationId, SessionId ni GUIDs: multiplican las series y saturan Loki. Initialize emite un Warning si detecta nombres sospechosos.
En Docker, MachineName toma el ID del contenedor si no fijas el hostname, y ese ID cambia en cada redeploy. Fija la variable de entorno HOST_NAME (o hostname: en el compose) para tener una label estable.
using Dinaup;
using Dinaup.Ntfy;
using Serilog.Events;
var loki = new Logs.LokiConfig
{
Endpoint = "https://loki.tu-dominio.com/loki/api/v1/push"
};
// Cliente ntfy: servidor + (opcional) token de acceso.
var ntfy = new NtfyClientC("https://ntfy.tu-dominio.com", "tk_tu-token");
// El tercer y cuarto argumento activan el push: cliente + topic.
Logs.Initialize(loki, ntfy, "mi-app-alertas");
Logs.SetLoggingLevel(LogEventLevel.Information);Con ntfy configurado, cada Warning, Error y Fatal publica una notificación en segundo plano. El nivel decide el topic de destino (mi-app-alertas-warning, -error, -fatal) y la prioridad ntfy. El envío está limitado a 60 notificaciones cada 20 segundos para no saturar el canal.
Antes de detener la aplicación llama a CloseAndFlush para enviar los datos pendientes.
Logs.CloseAndFlush();Nivel de log
El nivel mínimo se ajusta en caliente con SetLoggingLevel, o mediante la variable de entorno DINALOG_LEVEL (VERBOSE, DEBUG, INFORMATION, WARNING, ERROR, FATAL). Por defecto es Information.
Logs.SetLoggingLevel(LogEventLevel.Debug);Logs básicos y estructurados
Cada nivel (Verbose, Debug, Information, Warning, Error, Fatal) admite texto plano, plantilla estructurada con {Propiedad} y, además, una sobrecarga con la excepción como primer argumento.
Logs.Information("Usuario {UserId} inició sesión", userId);
Logs.Warning("Intentos de acceso fallidos {Count} para {User}", attempts, userEmail);
// Con excepción: la excepción va SIEMPRE como primer argumento.
Logs.Error(ex, "Error al procesar pedido {OrderId}", orderId);public sealed class StripeService
{
public async Task<string> CreateInvoice(Guid orderId)
{
using (Logs.BeginContext(nameof(StripeService), nameof(CreateInvoice)))
{
Logs.Debug("Generando factura para {OrderId}", orderId);
await Task.Delay(10); // tu lógica real aquí
var invoiceId = $"inv_{Guid.NewGuid():N}";
Logs.Information("Factura {InvoiceId} creada para {OrderId}", invoiceId, orderId);
return invoiceId;
}
}
}public sealed class JobWorker
{
public async Task RunAsync(IEnumerable<Guid> orderIds)
{
var correlationId = $"corr-{Guid.NewGuid():N}";
using (Logs.BeginCorrelationContext(nameof(JobWorker), nameof(RunAsync), correlationId))
{
Logs.Information("Procesando {Count} pedidos", orderIds.Count());
foreach (var orderId in orderIds)
{
Logs.Debug("Pedido {OrderId}", orderId);
await Task.Delay(5); // tu lógica real aquí
}
Logs.Information("Lote completado");
}
}
}Warning, Error y Fatal disparan además una notificación ntfy en segundo plano si inicializaste el logging con ntfy configurado. Para el detalle del cliente y las notificaciones manuales, ver Notificaciones ntfy.
Medición de operaciones
BeginMeasure mide la duración de un bloque de código y la registra al salir del using. Solo se registra si la operación supera el umbral (thresholdMs, por defecto 10 ms), para no inundar el log con operaciones triviales.
using (Logs.BeginMeasure("ImportarPedidos"))
{
await ImportarPedidosAsync();
}
// Al salir del using se registra la duración (si superó 10 ms).var metadata = new Dictionary<string, string>
{
{ "tenant", tenantId },
{ "origen", "cron" }
};
using (Logs.BeginMeasure("SincronizarStock", labelKey: "Operation", metadata: metadata, thresholdMs: 50))
{
await SincronizarStockAsync();
}public async Task ProcesarAsync()
{
// Sin argumentos: el nombre de la operación se toma del método (CallerMemberName).
using (Logs.BeginMeasure())
{
await TrabajoPesadoAsync();
}
}Helpers de ejecución
HandleAction y HandleActionAsync envuelven tu código con medición de tiempo y try/catch, registran la excepción si la hay y devuelven un HandleActionResult con:
IsOk: indica éxito.MessageException: detalle del error si falló.handledException: la excepción capturada (onullsi todo fue bien).
var result = Logs.HandleAction(
component: nameof(PagosService),
action: nameof(PagosService.Cobrar),
body: () =>
{
// Tu lógica
ProcesarCobro(pedidoId);
},
details: new { PedidoId = pedidoId }
);
if (result.IsOk == false)
{
// El error ya se registró dentro de HandleAction.
Logs.Error("No se pudo cobrar pedido {PedidoId}: {Error}", pedidoId, result.MessageException);
}var result = await Logs.HandleActionAsync(
component: nameof(ServicioAsync),
action: nameof(ServicioAsync.ProcesarAsync),
body: async () =>
{
await Task.Delay(10);
await ProcesarAsync();
},
details: new { Correlation = correlationId }
);
if (result.IsOk == false)
{
Logs.Error("Proceso async falló: {Error}", result.MessageException);
}HandleActionAsync admite un parámetro opcional adicional minReportMs (por defecto 0) para registrar solo cuando la operación supere ese tiempo.
Middleware de correlación (idea)
app.Use(async (context, next) =>
{
var componentName = "HttpRequest";
var actionName = context.Request.Path.Value ?? "/";
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? $"corr-{Guid.NewGuid():N}";
using (Logs.BeginCorrelationContext(componentName, actionName, correlationId))
{
await next();
}
});