Detección de anomalías para el monitoreo de aplicaciones
Detección de anomalías en métricas de aplicaciones: métodos estadísticos, z-scores, medias móviles, descomposición estacional y alertas sin fatiga.

Los umbrales estáticos son el enfoque por defecto para las alertas de monitoreo: "alertar cuando la tasa de errores supere el 5%" o "alertar cuando la latencia p99 supere los 500ms". Funcionan hasta que cambian tus patrones de tráfico: picos en festivos, caídas los fines de semana, crecimiento gradual. Una tasa de errores del 3% puede ser normal durante un pico de tráfico, pero alarmante a las 3 AM cuando solo los servicios internos llaman a tu API.
La detección de anomalías reemplaza los umbrales estáticos con líneas base dinámicas. En lugar de "¿está este número por encima de X?", pregunta "¿es este número inusual para este momento y contexto?". Esto reduce las falsas alertas durante cambios de tráfico esperados, mientras detecta incidentes reales que los umbrales estáticos pasan por alto.
Líneas base estadísticas con Z-Scores
Un z-score mide a cuántas desviaciones estándar está un valor de la media. Un z-score superior a 3 significa que el valor está a más de 3 desviaciones estándar de lo normal: aproximadamente un 0.3% de probabilidad de ocurrir de forma natural. Es el detector de anomalías más simple.
import numpy as np
from dataclasses import dataclass
@dataclass
class AnomalyResult:
value: float
mean: float
std: float
z_score: float
is_anomaly: bool
def detect_zscore_anomaly(
values: list[float],
current: float,
threshold: float = 3.0
) -> AnomalyResult:
"""Detect anomalies using z-score on recent history."""
arr = np.array(values)
mean = float(np.mean(arr))
std = float(np.std(arr))
# Avoid division by zero for constant metrics
if std == 0:
z_score = 0.0 if current == mean else float('inf')
else:
z_score = abs(current - mean) / std
return AnomalyResult(
value=current,
mean=mean,
std=std,
z_score=z_score,
is_anomaly=z_score > threshold
)
# Example: error rate over the last 24 hours (1-minute intervals)
error_rates = [0.02, 0.018, 0.022, 0.019, ...] # 1440 data points
current_rate = 0.085
result = detect_zscore_anomaly(error_rates, current_rate)
# z_score: 8.2, is_anomaly: True
# Mean 2%, current 8.5% → clearly anomalousMedia móvil exponencial (EMA)
Los z-scores tratan todos los datos históricos por igual. EMA da más peso a los valores recientes, adaptándose más rápido a las tendencias. El factor de suavizado alpha controla la rapidez con la que se ajusta la línea base: un alpha mayor significa una adaptación más rápida.
class EMADetector {
private ema: number | null = null;
private emaVariance: number = 0;
constructor(
private alpha: number = 0.1,
private threshold: number = 3.0
) {}
update(
value: number
): { isAnomaly: boolean; expected: number; deviation: number } {
if (this.ema === null) {
this.ema = value;
return { isAnomaly: false, expected: value, deviation: 0 };
}
// Update EMA (exponentially weighted moving average)
const prevEma = this.ema;
this.ema = this.alpha * value + (1 - this.alpha) * this.ema;
// Update variance estimate
const diff = value - prevEma;
this.emaVariance =
this.alpha * diff * diff +
(1 - this.alpha) * this.emaVariance;
const stdDev = Math.sqrt(this.emaVariance);
const deviation =
stdDev > 0 ? Math.abs(value - prevEma) / stdDev : 0;
return {
isAnomaly: deviation > this.threshold,
expected: prevEma,
deviation,
};
}
}
// Usage: detect latency anomalies
const latencyDetector = new EMADetector(0.05, 3.0);
// Feed in normal values to establish baseline
for (const latency of normalLatencies) {
latencyDetector.update(latency);
}
// Then check new values
const result = latencyDetector.update(850);
// { isAnomaly: true, expected: 120, deviation: 4.2 }Descomposición estacional
Muchas métricas tienen patrones diarios y semanales. El volumen de peticiones alcanza su pico al mediodía y cae a medianoche. Un aumento del 50% del tráfico al mediodía es normal; a las 3 AM es sospechoso. La descomposición estacional separa los componentes de tendencia, patrón estacional y residuo.
from dataclasses import dataclass
import numpy as np
@dataclass
class SeasonalBaseline:
hourly_means: np.ndarray # 24 values (one per hour)
hourly_stds: np.ndarray # 24 values
day_of_week_factors: np.ndarray # 7 values
def build_seasonal_baseline(
timestamps: list[float],
values: list[float],
min_weeks: int = 2
) -> SeasonalBaseline:
"""Build hourly baselines from historical data."""
from datetime import datetime
# Group values by hour-of-day
hourly_buckets: dict[int, list[float]] = {h: [] for h in range(24)}
dow_buckets: dict[int, list[float]] = {d: [] for d in range(7)}
for ts, val in zip(timestamps, values):
dt = datetime.fromtimestamp(ts)
hourly_buckets[dt.hour].append(val)
dow_buckets[dt.weekday()].append(val)
hourly_means = np.array([
np.mean(hourly_buckets[h]) if hourly_buckets[h] else 0
for h in range(24)
])
hourly_stds = np.array([
np.std(hourly_buckets[h]) if len(hourly_buckets[h]) > 1 else 1
for h in range(24)
])
global_mean = np.mean(values) if values else 1
day_factors = np.array([
np.mean(dow_buckets[d]) / global_mean if dow_buckets[d] else 1
for d in range(7)
])
return SeasonalBaseline(hourly_means, hourly_stds, day_factors)
def is_seasonal_anomaly(
baseline: SeasonalBaseline,
current_value: float,
hour: int,
day_of_week: int,
threshold: float = 3.0
) -> bool:
"""Check if value is anomalous for this time and day."""
expected = baseline.hourly_means[hour] * baseline.day_of_week_factors[day_of_week]
std = baseline.hourly_stds[hour]
if std == 0:
return current_value != expected
z = abs(current_value - expected) / std
return z > thresholdConstrucción de un pipeline de alertas
La detección de anomalías solo es útil si produce alertas accionables sin ahogar al ingeniero de guardia en ruido.
// ❌ Alerting on raw anomaly detection output
// Every z-score > 3 triggers a page
// Result: 15 alerts per day, most are benign blips
// On-call engineer ignores all alerts after 2 days
// ✅ Multi-stage alerting pipeline
interface AlertPipeline {
stage1_detection: {
description: "Raw anomaly detection on 1-minute data";
output: "Boolean: is this data point anomalous?";
};
stage2_confirmation: {
description: "Require N consecutive anomalous points";
config: { consecutiveRequired: 3; windowMinutes: 5 };
output: "Boolean: sustained anomaly confirmed?";
};
stage3_severity: {
description: "Classify severity based on deviation magnitude";
config: {
warning: "z-score 3-5 OR error rate increase 2-5x";
critical: "z-score > 5 OR error rate increase > 5x";
};
};
stage4_dedup: {
description: "Suppress duplicate alerts for same incident";
config: { silenceMinutes: 30; groupBy: "metric_name + service" };
};
}class AlertManager {
private activeAlerts = new Map<string, { since: Date; count: number }>();
shouldAlert(
metricKey: string,
isAnomaly: boolean,
severity: "warning" | "critical"
): { alert: boolean; action: "page" | "slack" | "none" } {
const existing = this.activeAlerts.get(metricKey);
if (!isAnomaly) {
// Anomaly resolved
if (existing) {
this.activeAlerts.delete(metricKey);
return { alert: true, action: "slack" }; // Resolution notification
}
return { alert: false, action: "none" };
}
if (existing) {
existing.count++;
// Already alerted, suppress until silence window expires
return { alert: false, action: "none" };
}
// New anomaly
this.activeAlerts.set(metricKey, { since: new Date(), count: 1 });
return {
alert: true,
action: severity === "critical" ? "page" : "slack",
};
}
}Combinación de múltiples señales
La detección de anomalías con una sola métrica produce falsos positivos. Combinar métricas correlacionadas mejora drásticamente la precisión.
interface CorrelatedCheck {
primary: { metric: string; isAnomaly: boolean };
supporting: Array<{ metric: string; isAnomaly: boolean }>;
confidence: number;
}
function correlatedAnomalyCheck(
checks: CorrelatedCheck
): { isReal: boolean; confidence: number } {
if (!checks.primary.isAnomaly) {
return { isReal: false, confidence: 0 };
}
const supportingAnomaly = checks.supporting.filter(
(s) => s.isAnomaly
).length;
const totalSupporting = checks.supporting.length;
// Higher confidence when multiple related metrics are anomalous
const correlationScore =
totalSupporting > 0 ? supportingAnomaly / totalSupporting : 0;
const confidence = 0.5 + 0.5 * correlationScore;
return {
isReal: confidence > 0.6,
confidence,
};
}
// Example: high error rate is more likely real if:
// - Latency is also elevated (correlated)
// - Traffic volume is normal (not a traffic spike causing errors)
// - CPU usage is elevated (resource pressure)
const check: CorrelatedCheck = {
primary: { metric: "error_rate", isAnomaly: true },
supporting: [
{ metric: "p99_latency", isAnomaly: true },
{ metric: "request_rate", isAnomaly: false },
{ metric: "cpu_usage", isAnomaly: true },
],
confidence: 0,
};
// 2/3 supporting anomalies → confidence: 0.83 → alertPuntos clave
- Los umbrales estáticos fallan cuando cambian los patrones de tráfico: reemplaza "alertar por encima de X" con líneas base dinámicas que tengan en cuenta los patrones por hora del día y día de la semana
- Los z-scores son el detector de anomalías más simple: si un valor está a más de 3 desviaciones estándar de la media, es inusual; esto detecta el 99.7% de las anomalías genuinas con un mínimo de falsos positivos
- Exige anomalías sostenidas antes de alertar: un solo punto anómalo puede ser ruido; requiere 3 o más puntos anómalos consecutivos para confirmar un incidente real
- Combina métricas correlacionadas para reducir falsos positivos: una tasa de errores alta más latencia alta más tráfico normal es más probablemente un incidente real que una tasa de errores alta por sí sola
- Las líneas base estacionales evitan falsas alertas por hora del día: construye líneas base separadas para cada hora y día de la semana para no alertar sobre patrones de tráfico diarios normales


