Saltar al contenido principal

🚨 Health Checks, Alerting y APM

Health Checks en ASP.NET Core

// Program.cs
builder.Services.AddHealthChecks()
// BD — intenta conectar y ejecutar una query simple
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("Default")!,
healthQuery: "SELECT 1",
name: "sqlserver",
failureStatus: HealthStatus.Unhealthy,
tags: ["db", "sqlserver"])

// Redis
.AddRedis(
redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
name: "redis",
failureStatus: HealthStatus.Degraded, // sin caché funciona, pero degradado
tags: ["cache", "redis"])

// Servicio externo
.AddUrlGroup(
uri: new Uri("https://api.pagos.com/health"),
name: "payment-gateway",
failureStatus: HealthStatus.Degraded,
tags: ["external"])

// Health check custom
.AddCheck<PendingOrdersHealthCheck>("pending-orders", tags: ["business"]);

// Exponer endpoints diferenciados
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
// Liveness: ¿la app está viva? Solo checks críticos.
// Si falla → Kubernetes reinicia el pod
Predicate = check => !check.Tags.Contains("external"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
// Readiness: ¿la app puede atender tráfico?
// Si falla → Kubernetes deja de enviar tráfico al pod (pero no lo reinicia)
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// Health check custom — lógica de negocio
public class PendingOrdersHealthCheck : IHealthCheck
{
private readonly IOrderRepository _repository;

public PendingOrdersHealthCheck(IOrderRepository repository)
=> _repository = repository;

public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var pendingCount = await _repository.CountPendingAsync(cancellationToken);

return pendingCount switch
{
< 100 => HealthCheckResult.Healthy($"{pendingCount} pedidos pendientes"),
< 1000 => HealthCheckResult.Degraded($"⚠️ {pendingCount} pedidos pendientes — posible retraso"),
_ => HealthCheckResult.Unhealthy($"🔴 {pendingCount} pedidos pendientes — sistema saturado")
};
}
}

Kubernetes Probes

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
template:
spec:
containers:
- name: orders-api
image: myregistry/orders-api:latest
ports:
- containerPort: 8080

# Liveness: si falla, Kubernetes reinicia el contenedor
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10 # esperar N segundos antes del primer check
periodSeconds: 30 # cada 30s
failureThreshold: 3 # 3 fallos consecutivos → reiniciar

# Readiness: si falla, no enviar tráfico (pero no reiniciar)
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 2 # 2 fallos → quitar del load balancer

# Startup: para apps lentas en arrancar (evita que liveness las mate)
startupProbe:
httpGet:
path: /health/live
port: 8080
failureThreshold: 30 # hasta 30 * 10s = 5 minutos para arrancar
periodSeconds: 10

Diferencia clave Liveness vs Readiness:

  • Liveness: ¿está el proceso vivo? Un deadlock o estado corrupto → reiniciar.
  • Readiness: ¿puede atender tráfico? Migraciones en curso, warmup de caché, dependencias no disponibles → no enviar tráfico, pero no reiniciar.

Alerting y On-Call

Principios de buenas alertas

Alerta sobre síntomas (el usuario está afectado), no sobre causas internas.

# ❌ Alerta sobre causa interna — puede que no afecte a usuarios
- alert: CPUAlta
expr: cpu_usage > 80
# ¿Y? Quizás la app funciona perfectamente con CPU alta

# ✅ Alerta sobre síntoma — el usuario definitivamente está siendo afectado
- alert: ErrorRateAlta
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m])) > 0.01
annotations:
summary: "Tasa de errores > 1%"
runbook: "https://wiki.empresa.com/runbooks/error-rate-alta"

SLO Burn Rate Alerts — más inteligentes que umbrales simples

# Prometheus — alertas basadas en burn rate del error budget
groups:
- name: orders-api-slo-alerts
rules:
# Alerta CRÍTICA: consumiendo budget a 14x — quedaremos sin budget en ~2 horas
- alert: ErrorBudgetBurnRateCritico
expr: |
(
sum(rate(http_requests_total{service="orders",status=~"5.."}[1h]))
/ sum(rate(http_requests_total{service="orders"}[1h]))
) > (14 * 0.001) # 14x el error rate permitido (0.1%)
for: 2m
labels:
severity: critical
annotations:
summary: "🔴 Burn rate crítico: error budget agotado en ~2h"
runbook: "https://wiki/runbooks/orders-api-burn-rate"

# Alerta WARNING: consumiendo budget a 6x — quedaremos sin budget en ~1 día
- alert: ErrorBudgetBurnRateWarning
expr: |
(
sum(rate(http_requests_total{service="orders",status=~"5.."}[6h]))
/ sum(rate(http_requests_total{service="orders"}[6h]))
) > (6 * 0.001)
for: 15m
labels:
severity: warning
annotations:
summary: "⚠️ Burn rate elevado: error budget agotado en ~1 día"

¿Por qué burn rate > umbral simple?

  • Un umbral simple error_rate > 1% genera alertas aunque sea por 1 minuto (ruido).
  • El burn rate mide si estás consumiendo el presupuesto de errores más rápido de lo sostenible.
  • Reduce falsos positivos y asegura que cada alerta sea accionable.

Cada alerta necesita un Runbook

# Runbook: ErrorRateAlta en orders-api

## ¿Qué significa esta alerta?
La tasa de errores HTTP 5xx supera el 1% en los últimos 5 minutos.

## Impacto en el usuario
Los usuarios pueden estar viendo errores al crear o consultar pedidos.

## Pasos de diagnóstico
1. Ver logs en Kibana: `service:orders-api AND level:ERROR`
2. Ver trazas recientes en Jaeger: buscar spans con error=true
3. Revisar deploys recientes: `kubectl rollout history deployment/orders-api`
4. Revisar métricas de BD: ¿hay lentitud en queries? ¿connection pool exhausto?

## Acciones
- Si fue un deploy reciente: `kubectl rollout undo deployment/orders-api`
- Si es la BD: escalar réplicas de lectura o revisar slow query log
- Si es un servicio externo (ej: payment gateway): activar circuit breaker

## Escalado
Si no se resuelve en 15 minutos: escalar a @backend-team-lead

Application Performance Monitoring (APM)

Application Insights en Azure

// Instalación
// dotnet add package Microsoft.ApplicationInsights.AspNetCore

// Program.cs
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];

// Sampling adaptativo: captura el 100% cuando hay poco tráfico,
// reduce automáticamente cuando el tráfico es alto
options.EnableAdaptiveSampling = true;
});
// Telemetría personalizada con TelemetryClient
public class OrderService
{
private readonly TelemetryClient _telemetry;

public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
var stopwatch = Stopwatch.StartNew();

// Evento de negocio — aparece en "Custom Events" en App Insights
_telemetry.TrackEvent("OrderCreated", new Dictionary<string, string>
{
["CustomerId"] = cmd.CustomerId.ToString(),
["Region"] = cmd.Region,
["PaymentMethod"] = cmd.PaymentMethod
},
new Dictionary<string, double>
{
["OrderValue"] = (double)cmd.TotalAmount,
["ItemCount"] = cmd.Items.Count
});

// Métrica — aparece en "Custom Metrics"
_telemetry.TrackMetric("Order.ProcessingTime", stopwatch.ElapsedMilliseconds);

// Dependencia externa — aparece en "Dependencies" y en el mapa de aplicación
var dep = new DependencyTelemetry
{
Name = "InventoryService.Reserve",
Type = "HTTP",
Target = "inventory-service",
Success = true,
Duration = TimeSpan.FromMilliseconds(45)
};
_telemetry.TrackDependency(dep);
}
}

Sampling — por qué no capturar el 100%

EscenarioTráfico% CapturarRazonamiento
DesarrolloBajo100%Queremos ver todo
Producción (bajo tráfico)< 1 RPS100%El coste es asumible
Producción (alto tráfico)> 100 RPS5-20%Coste de almacenamiento y procesamiento
Producción (muy alto)> 1000 RPS1-5%Aún representativo estadísticamente
// Sampling fijo: capturar exactamente el X%
builder.Services.Configure<TelemetryConfiguration>(config =>
{
config.DefaultTelemetrySink.TelemetryProcessorChainBuilder
.UseAdaptiveSampling(maxTelemetryItemsPerSecond: 5) // max 5 items/seg
.Build();
});

Trade-off clave: el sampling reduce el coste pero puede perder eventos raros e importantes (como un error que ocurre 1 vez cada 10.000 requests). Solución: never sample errors — siempre capturar el 100% de errores, hacer sampling solo del tráfico exitoso.


Resumen de Herramientas por Pilar

PilarSelf-hostedCloud
LogsSeq + SerilogAzure Monitor, Datadog Logs
MétricasPrometheus + GrafanaAzure Monitor Metrics, Datadog
TrazasJaeger, Zipkin, TempoAzure App Insights, Datadog APM
Todo en unoGrafana Stack (Loki+Tempo+Mimir)Datadog, New Relic, Dynatrace