Librería de observabilidad ligera para aplicaciones .NET 8+ que centraliza auditoría de solicitudes HTTP, captura automática de errores, ejecución de tareas en segundo plano y resiliencia en peticiones externas. Registra logs detallados en base de datos con Entity Framework Core, generando identificadores únicos para trazabilidad e inspección de errores. Soporta formato RFC 7807 (ProblemDetails) y formato legacy para retrocompatibilidad.
$ dotnet add package Reec.InspectionReec.Inspection es una librería de observabilidad ligera para aplicaciones .NET 8+ que centraliza:
Todo esto usando Entity Framework Core y una configuración sencilla basada en opciones (ReecExceptionOptions).
Si solo quieres verlo funcionando en minutos, sigue esta sección.
Para más detalles, baja a la 👉 Guía completa.
dotnet add package Reec.Inspection
dotnet add package Reec.Inspection.SqlServer
Program.cs)builder.Services.AddReecInspection<InspectionDbContext>(
options => options.UseSqlServer(builder.Configuration.GetConnectionString("default")),
options =>
{
options.ApplicationName = "Reec.Inspection.Api"; // Obligatorio
options.SystemTimeZoneId = "SA Pacific Standard Time"; // Recomendado
options.EnableProblemDetails = true; // Opcional
});
var app = builder.Build();
app.UseReecInspection(); // Registra los middlewares de auditoría y captura de errores
app.MapControllers();
app.Run();
Con esto obtienes:
LogAudit) para requests HTTP.LogHttp) para excepciones no controladas.[HttpGet("error")]
public IActionResult GetError()
{
var x = 1 / 0; // Error intencional
return Ok();
}
<div align="center">
<img src="images/QR Plin.jpeg" alt="Plin QR Code" width="300"/>
</div>
<div align="center">
</div>Ese error se registra automáticamente en la tabla LogHttp (y puede devolverse como ProblemDetails si está activado).
Ejemplo rápido para LogAudit:
options.LogAudit.EnableClean = true;
options.LogAudit.CronValue = "0 2 * * *"; // Todos los días a las 2 a.m.
options.LogAudit.DeleteDays = 10; // Mantiene solo los últimos 10 días
Cada tipo (LogAudit, LogHttp, LogEndpoint, LogJob) tiene su propio worker de limpieza opcional.
Si Reec.Inspection está ayudando a optimizar tu trabajo y te gustaría contribuir al desarrollo continuo de esta librería, puedes hacerlo a través de Plin (Perú):
Yape/Plin
Tu apoyo ayuda a mantener el proyecto actualizado con nuevas características, correcciones de bugs y documentación mejorada. ¡Toda contribución es valorada! 🙏
ReecExceptionOptionsSystemTimeZoneIdApplicationNameEnableGlobalDbSave / IsSaveDB)LogHttp, LogAudit)IWorker)AddReecInspectionResilience)Registro principal en Program.cs:
builder.Services.AddReecInspection<InspectionDbContext>(
options => options.UseSqlServer(builder.Configuration.GetConnectionString("default")),
options =>
{
options.ApplicationName = "Reec.Inspection.Api";
options.SystemTimeZoneId = "SA Pacific Standard Time";
options.EnableProblemDetails = true;
options.EnableGlobalDbSave = true;
});
var app = builder.Build();
app.UseReecInspection();
AddReecInspection:
DbContext derivado de InspectionDbContext con DbContextPool.LogAuditMiddleware, LogHttpMiddleware).IWorker, IDateTimeService y workers de limpieza (CleanLog*Worker) según configuración.ProblemDetails.UseReecInspection:
ReecExceptionOptions.Orden recomendado:
app.UseResponseCompression();
app.UseReecInspection();
app.UseOutputCache();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
AddReecException<TDbContext> (Legacy)[Obsolete] para compatibilidad.AddReecInspection<TDbContext> (Recomendado)DbContextPool.ReecExceptionOptions mediante Action<ReecExceptionOptions>.IWorker.Ejemplo:
builder.Services.AddReecInspection<InspectionDbContext>(
options => options.UseSqlServer(connString),
options =>
{
options.ApplicationName = "MyService.Api";
options.EnableGlobalDbSave = true;
options.LogHttp.IsSaveDB = true;
options.LogAudit.IsSaveDB = true;
});
ReecExceptionOptionsReecExceptionOptions centraliza la configuración global.
| Propiedad | Descripción | Default |
|---|---|---|
ApplicationName | Nombre de la app que genera los logs. | null |
ApplicationErrorMessage | Mensaje mostrado cuando ocurre un error al intentar guardar información en la base de datos. | Ocurrió un error al guardar log en Base de Datos. |
InternalServerErrorMessage | Mensaje genérico utilizado para errores internos del sistema. | Error no controlado del sistema. |
SystemTimeZoneId | Zona horaria usada para registrar fechas y programar cron. | "SA Pacific Standard Time" |
EnableMigrations | Ejecuta migraciones automáticas al inicio. | true |
EnableProblemDetails | Respuestas de error en formato ProblemDetails. | false |
EnableGlobalDbSave | Habilita/deshabilita escritura global en BD. | true |
MinCategory | Categoría mínima a registrar. | Unauthorized (401) |
Cada módulo tiene opciones propias (LogAudit, LogHttp, LogJob, LogEndpoint):
Schema: esquema de base de datos.TableName: nombre de la tabla.IsSaveDB: habilita/deshabilita persistencia.EnableClean: activa worker de limpieza.CronValue: expresión CRON para limpieza. Puedes usar https://crontab.guru para generar y validar expresiones CRON.DeleteDays: días hacia atrás a conservar.DeleteBatch: tamaño del lote de borrado.Ejemplo para LogAudit con tablas existentes (sin migraciones):
options.EnableMigrations = false;
options.LogAudit.Schema = "Inspection";
options.LogAudit.TableName = "LogAudit";
options.LogAudit.IsSaveDB = true;
options.LogAudit.EnableClean = true;
options.LogAudit.CronValue = "0 2 * * *";
options.LogAudit.DeleteDays = 15;
options.LogAudit.DeleteBatch = 500;
Aplica el mismo patrón para LogHttp, LogJob y LogEndpoint.
EnableBuffering en LogHttp y LogAuditLa propiedad EnableBuffering está disponible únicamente en los módulos LogHttp y LogAudit, ya que estos middlewares necesitan leer el cuerpo (body) de las peticiones y respuestas HTTP para registrarlas en la base de datos.
¿Qué hace EnableBuffering?
Cuando está habilitado (true), permite que el stream del request y response pueda ser leído múltiples veces, lo cual es necesario para capturar el contenido sin afectar el flujo normal de la aplicación.
¿Cuándo desactivarlo?
Si ya tienes un middleware superior en tu pipeline que gestiona el buffering del request/response (por ejemplo, para logging personalizado, transformación de contenido, o compresión), puedes desactivar EnableBuffering en estos módulos para evitar redundancia y mejorar el rendimiento.
Ejemplo de configuración:
options.LogHttp.EnableBuffering = true; // Por defecto
options.LogAudit.EnableBuffering = false; // Desactivado si hay middleware superior que ya gestiona buffering
Nota:
LogJobyLogEndpointno tienen esta propiedad ya que no interactúan directamente con streams HTTP del pipeline de ASP.NET Core.
SystemTimeZoneIdTodas las fechas registradas en los logs y workers usan esta zona horaria:
CronValue.options.SystemTimeZoneId = "SA Pacific Standard Time";
Para ver las zonas disponibles:
var zones = TimeZoneInfo.GetSystemTimeZones();
Si el ID es inválido, la inicialización de IDateTimeService lanzará excepción.
ApplicationNameObligatorio para distinguir qué sistema originó cada registro.
options.ApplicationName = "Billing.Api";
Se utiliza en todas las tablas de log como columna de referencia.
options.EnableGlobalDbSave = true; // Si es false, no se persisten logs en BD.
options.LogAudit.IsSaveDB = true;
options.LogHttp.IsSaveDB = true;
options.LogJob.IsSaveDB = true;
options.LogEndpoint.IsSaveDB = true;
Desactivar por módulo es útil para escenarios donde solo quieres ciertos tipos de trazas.
LogHttp, LogAudit)LogHttp — Errores del pipelineEjemplo:
[HttpGet("test-error")]
public IActionResult TestError()
{
var value = 10 / 0;
return Ok(value);
}
LogHttpMiddleware:
Exception, StackTrace, Path, TraceIdentifier, etc.ProblemDetails si está habilitado.LogAudit — Auditoría HTTPLogAuditMiddleware:
ExcludePathsRequestBodyMaxSizeResponseBodyMaxSizeEnableBufferingEjemplo de exclusión:
options.LogAudit.ExcludePaths = new[] { "swagger", "health", "index" };
IWorker)IWorker expone:
RunFunction: lógica principal.RunFunctionException: manejo custom de errores.IsLightExecution: solo registra fallos cuando es true.Enqueued, Processing, Succeeded, Failed) en LogJob.Uso recomendado para jobs periódicos (patrón similar a los CleanLog*Worker).
public class SampleJobWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public SampleJobWorker(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
using var scope = _scopeFactory.CreateScope();
var worker = scope.ServiceProvider.GetRequiredService<IWorker>();
worker.NameJob = nameof(SampleJobWorker);
worker.CreateUser = "System";
worker.IsLightExecution = false;
worker.RunFunction = service => ProcessAsync(service, stoppingToken);
await worker.ExecuteAsync(stoppingToken);
}
}
private static async Task<string> ProcessAsync(IServiceProvider services, CancellationToken ct)
{
var dbContextService = services.GetRequiredService<IDbContextService>();
var db = dbContextService.GetDbContext();
// Lógica de negocio aquí
await Task.Delay(1000, ct);
return "Proceso completado correctamente.";
}
}
Registrar el worker:
builder.Services.AddHostedService<SampleJobWorker>();
Para iniciar una tarea en segundo plano desde un endpoint HTTP sin bloquear la respuesta:
[ApiController]
[Route("api/[controller]")]
public class JobsController : ControllerBase
{
private readonly IServiceScopeFactory _scopeFactory;
public JobsController(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
[HttpPost("start-clean-temp")]
public IActionResult StartCleanTemp()
{
var scope = _scopeFactory.CreateScope();
var worker = scope.ServiceProvider.GetRequiredService<IWorker>();
worker.NameJob = "CleanTemporaryFiles";
worker.CreateUser = "System";
worker.IsLightExecution = true;
worker.RunFunction = svc => ProcessAsync(svc);
_ = worker.ExecuteAsync().ContinueWith(_ =>
{
scope.Dispose();
});
return Ok("Tarea en segundo plano iniciada.");
}
private static async Task<string> ProcessAsync(IServiceProvider services)
{
var dbContextService = services.GetRequiredService<IDbContextService>();
var db = dbContextService.GetDbContext();
// Lógica puntual
await Task.Delay(2000);
return "Limpieza de temporales completada.";
}
}
Notas:
Task.Run externo: IWorker maneja la ejecución asíncrona y logging.LogJob.AddReecInspectionResilience)Esta extensión integra:
LogEndpointHandler: registra requests/responses a servicios externos.var httpBuilder = builder.Services.AddHttpClient("PlaceHolder", httpClient =>
{
httpClient.DefaultRequestHeaders.Clear();
httpClient.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
});
builder.Services.AddReecInspectionResilience(httpBuilder);
Uso:
public class ExternalController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
public ExternalController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[HttpGet("posts")]
public async Task<IActionResult> GetPosts()
{
var client = _httpClientFactory.CreateClient("PlaceHolder");
var response = await client.GetAsync("/posts");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return Content(content, "application/json");
}
}
AddReecInspectionResilience configura por defecto:
HttpRequestMessage.Options.Para usar PostgreSQL (u otro proveedor soportado por EF Core), hereda de InspectionDbContext y genera una migración:
public class InspectionPgContext : InspectionDbContext
{
public InspectionPgContext(DbContextOptions<InspectionPgContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Ejemplo: esquema por defecto
modelBuilder.HasDefaultSchema("inspection");
}
}
Registro:
builder.Services.AddReecInspection<InspectionPgContext>(
options => options.UseNpgsql(builder.Configuration.GetConnectionString("Postgres")),
options =>
{
options.ApplicationName = "Reec.Pg.Api";
options.EnableMigrations = true; // O manejar migraciones externamente
});
ApplicationName y SystemTimeZoneId.EnableMigrations en producción y aplicar migraciones vía CI/CD.Schema dedicado (ej. "Inspection") para aislar tus tablas de log.DeleteDays y DeleteBatch según volumen de logs.IsLightExecution = true para jobs muy frecuentes donde solo te interesen errores.LogAudit (swagger, health, etc.).Reec.Inspection mantiene compatibilidad total con el sistema de excepciones del proyecto Reec original mediante ReecException y ReecMessage.
Este modo es útil cuando:
Para usar el modo legacy, establece EnableProblemDetails = false (es el valor por defecto):
builder.Services.AddReecInspection<InspectionDbContext>(
options => options.UseSqlServer(connString),
options =>
{
options.ApplicationName = "Legacy.Api";
options.EnableProblemDetails = false; // Modo legacy activado
});
Las categorías están definidas en el enum Category y representan diferentes tipos de respuestas:
| Categoría | Valor | HTTP Status | Uso |
|---|---|---|---|
OK | 200 | 200 | Operación exitosa |
PartialContent | 206 | 206 | Consulta exitosa sin contenido |
Unauthorized | 401 | 401 | Autenticación requerida |
Forbidden | 403 | 403 | Sin permisos suficientes |
Warning | 460 | 400 | Validación de campos |
BusinessLogic | 465 | 400 | Errores controlados de negocio |
BusinessLogicLegacy | 470 | 400 | Errores controlados de sistemas externos |
InternalServerError | 500 | 500 | Errores no controlados |
BadGateway | 502 | 502 | Error en sistema externo |
GatewayTimeout | 504 | 504 | Timeout en sistema externo |
[HttpPost("create-user")]
public IActionResult CreateUser(CreateUserRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email))
{
throw new ReecException(Category.Warning, "El correo electrónico es obligatorio.");
}
// Lógica de creación...
return Ok();
}
Respuesta JSON:
{
"id": 42,
"path": "/create-user",
"traceIdentifier": "0HNGTMGA752BQ:00000001",
"category": 460,
"categoryDescription": "Warning",
"message": ["El correo electrónico es obligatorio."]
}
[HttpPost("validate-form")]
public IActionResult ValidateForm(FormData data)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(data.Name))
errors.Add("El nombre es obligatorio.");
if (data.Age < 18)
errors.Add("Debe ser mayor de 18 años.");
if (errors.Any())
{
throw new ReecException(Category.Warning, errors);
}
return Ok();
}
Respuesta JSON:
{
"id": 43,
"path": "/validate-form",
"traceIdentifier": "0HNGTMGA752BQ:00000002",
"category": 460,
"categoryDescription": "Warning",
"message": [
"El nombre es obligatorio.",
"Debe ser mayor de 18 años."
]
}
[HttpPost("transfer")]
public IActionResult Transfer(TransferRequest request)
{
var account = GetAccount(request.AccountId);
if (account.Balance < request.Amount)
{
throw new ReecException(
Category.BusinessLogic,
"Saldo insuficiente para realizar la transferencia."
);
}
// Procesar transferencia...
return Ok();
}
Respuesta JSON:
{
"id": 44,
"path": "/transfer",
"traceIdentifier": "0HNGTMGA752BQ:00000003",
"category": 465,
"categoryDescription": "Business Logic",
"message": ["Saldo insuficiente para realizar la transferencia."]
}
[HttpPost("import-data")]
public IActionResult ImportData(ImportRequest request)
{
try
{
var result = ExternalService.ProcessFile(request.FilePath);
return Ok(result);
}
catch (Exception ex)
{
throw new ReecException(
Category.BusinessLogicLegacy,
"Error al procesar el archivo de importación.",
ex.Message // ExceptionMessage original
);
}
}
Respuesta JSON:
{
"id": 45,
"path": "/import-data",
"traceIdentifier": "0HNGTMGA752BQ:00000004",
"category": 470,
"categoryDescription": "Business Logic Legacy",
"message": ["Error al procesar el archivo de importación."]
}
Nota: En la base de datos (
LogHttp), el campoExceptionMessagecontendrá el mensaje original de la excepción, mientras queMessageUserguarda el mensaje amigable para el cliente.
[HttpGet("external-data")]
public async Task<IActionResult> GetExternalData()
{
try
{
var client = _httpClientFactory.CreateClient("ExternalApi");
var response = await client.GetAsync("/data");
response.EnsureSuccessStatusCode();
return Ok(await response.Content.ReadAsStringAsync());
}
catch (HttpRequestException ex)
{
throw new ReecException(
Category.BadGateway,
"No se pudo conectar con el servicio externo.",
ex.Message,
ex.InnerException // Se preserva InnerException
);
}
}
LogHttpMiddleware detecta automáticamente si la excepción es del tipo ReecException:
ReecMessage configurado.HttpStatusCode según la categoría:
Warning, BusinessLogic, BusinessLogicLegacy → 400 Bad RequestLogHttp (si está habilitado).ReecMessage serializado como JSON.✅ Retrocompatibilidad: Los clientes existentes siguen funcionando sin cambios.
✅ Flexibilidad: Control total sobre la estructura de respuesta.
✅ Trazabilidad: Cada error registra un Id único en base de datos.
✅ Claridad: Las categorías personalizadas son más descriptivas que códigos HTTP estándar.
Reec.Inspection soporta el estándar RFC 7807 (Problem Details for HTTP APIs) para respuestas de error estructuradas y consistentes.
Este modo es recomendado cuando:
UseExceptionHandler.Para activar el modo ProblemDetails, establece EnableProblemDetails = true:
builder.Services.AddReecInspection<InspectionDbContext>(
options => options.UseSqlServer(connString),
options =>
{
options.ApplicationName = "Modern.Api";
options.EnableProblemDetails = true; // Modo actual activado
options.InternalServerErrorMessage = "Error no controlado del sistema.";
});
Cuando ocurre una excepción, la respuesta sigue el formato estándar RFC 7807:
{
"title": "Internal Server Error",
"status": 500,
"detail": "Error no controlado del sistema.",
"instance": "/api/demo/error",
"id": 1,
"category": 500,
"traceIdentifier": "0HNGTMGA752BQ:00000003"
}
| Campo | Tipo | Descripción |
|---|---|---|
title | string | Nombre legible de la categoría de error |
status | int | Código HTTP estándar |
detail | string | Mensaje de error descriptivo para el usuario |
instance | string | Path del endpoint que generó el error |
id | int | ID del registro en la tabla LogHttp (0 si no se guardó) |
category | int | Código de categoría interno (compatible con modo legacy) |
traceIdentifier | string | Identificador único para correlación de logs |
El middleware captura automáticamente cualquier excepción no controlada:
[ApiController]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
[HttpGet("InternalServerError")]
public IActionResult InternalServerError()
{
var numerador = 1;
var denominador = 0;
var dividendo = numerador / denominador; // DivideByZeroException
return Ok(dividendo);
}
}
Request:
GET /api/demo/InternalServerError
Response (500 Internal Server Error):
{
"title": "Internal Server Error",
"status": 500,
"detail": "Error no controlado del sistema.",
"instance": "/api/demo/InternalServerError",
"id": 1,
"category": 500,
"traceIdentifier": "0HNGTMGA752BQ:00000003"
}
Aunque uses ProblemDetails, puedes seguir lanzando ReecException para controlar la categoría:
[HttpPost("validate")]
public IActionResult Validate(UserInput input)
{
if (string.IsNullOrWhiteSpace(input.Email))
{
throw new ReecException(
Category.Warning,
"El correo electrónico es obligatorio."
);
}
return Ok();
}
Response (400 Bad Request):
{
"title": "Warning",
"status": 400,
"detail": "El correo electrónico es obligatorio.",
"instance": "/validate",
"id": 2,
"category": 460,
"traceIdentifier": "0HNGTMGA752BQ:00000004"
}
El middleware mapea las categorías personalizadas a códigos HTTP estándar:
| Categoría | Código Interno | HTTP Status | Title |
|---|---|---|---|
OK | 200 | 200 | OK |
PartialContent | 206 | 206 | Partial Content |
Unauthorized | 401 | 401 | Unauthorized |
Forbidden | 403 | 403 | Forbidden |
Warning | 460 | 400 | Warning |
BusinessLogic | 465 | 400 | Business Logic |
BusinessLogicLegacy | 470 | 400 | Business Logic Legacy |
InternalServerError | 500 | 500 | Internal Server Error |
BadGateway | 502 | 502 | Bad Gateway |
GatewayTimeout | 504 | 504 | Gateway Timeout |
Importante: Las categorías
Warning,BusinessLogicyBusinessLogicLegacyse traducen a 400 Bad Request para cumplir con los estándares HTTP.
Todas las respuestas de error en modo ProblemDetails incluyen un header personalizado:
EnableProblemDetails: true
Esto permite a los clientes detectar automáticamente el formato de respuesta.
Independientemente del formato de respuesta (Legacy o ProblemDetails), todos los errores se registran en la tabla LogHttp con:
✅ Estándar RFC 7807: Compatible con herramientas y bibliotecas de la industria.
✅ Integración nativa: Funciona con UseExceptionHandler de ASP.NET Core.
✅ Extensibilidad: Puedes agregar propiedades personalizadas en Extensions.
✅ Herramientas: Swagger, Postman y otros clientes entienden el formato automáticamente.
✅ Trazabilidad: Mantiene traceIdentifier para correlación con logs.
| Aspecto | Modo Legacy | Modo ProblemDetails |
|---|---|---|
| Formato | ReecMessage personalizado | RFC 7807 estándar |
| Retrocompatibilidad | ✅ Con Reec original | ❌ Requiere actualizar clientes |
| Estándar industria | ❌ | ✅ |
| Categorías custom | ✅ 460, 465, 470 | ✅ Traducidas a 400 |
| Tooling support | ⚠️ Limitado | ✅ Amplio |
| Persistencia BD | ✅ | ✅ |
| TraceIdentifier | ✅ | ✅ |
¿Tienes ideas, sugerencias o encontraste un bug?
Nota: Este proyecto es una reescritura completa del proyecto original Reec, con arquitectura mejorada, soporte para .NET 8+, y nuevas características como workers de limpieza, resiliencia HTTP y modos de respuesta intercambiables.
Construido con ❤️ para la comunidad .NET
⭐ Dale una estrella en GitHub si te fue útil