Esta documentación está en fase de desarrollo y puede contener errores.

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 (o null si 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();
    }
});

On this page