Anomalieerkennung für Application Monitoring
Anomalieerkennung für Anwendungsmetriken: statistische Methoden, Z-Scores, gleitende Durchschnitte, saisonale Zerlegung und Alarme ohne Alert Fatigue.

Statische Schwellenwerte sind der Standardansatz für Monitoring-Alerts: "Alarm, wenn die Fehlerrate 5% überschreitet" oder "Alarm, wenn die p99-Latenz 500ms überschreitet". Sie funktionieren, bis sich deine Verkehrsmuster ändern: Feiertagsspitzen, Wochenendflauten, allmähliches Wachstum. Eine Fehlerrate von 3% kann während eines Verkehrsspitze normal sein, aber um 3 Uhr morgens alarmierend, wenn nur interne Dienste deine API aufrufen.
Anomalieerkennung ersetzt statische Schwellenwerte durch dynamische Baselines. Statt "liegt dieser Wert über X?" fragt sie "ist dieser Wert ungewöhnlich für diesen Zeitpunkt und Kontext?". Das reduziert Fehlalarme bei erwarteten Verkehrsänderungen und erkennt gleichzeitig echte Vorfälle, die statische Schwellenwerte übersehen.
Statistische Baselines mit Z-Scores
Ein Z-Score misst, wie viele Standardabweichungen ein Wert vom Mittelwert entfernt ist. Ein Z-Score über 3 bedeutet, dass der Wert mehr als 3 Standardabweichungen vom Normalen abweicht — ungefähr eine 0.3%-Chance, natürlich aufzutreten. Das ist der einfachste Anomaliedetektor.
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 anomalousExponentiell gleitender Durchschnitt (EMA)
Z-Scores behandeln alle historischen Daten gleich. EMA gewichtet neuere Werte stärker und passt sich schneller an Trends an. Der Glättungsfaktor alpha steuert, wie schnell sich die Baseline anpasst — ein höheres alpha bedeutet schnellere Anpassung.
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 }Saisonale Zerlegung
Viele Metriken haben tägliche und wöchentliche Muster. Das Anfragevolumen erreicht mittags seinen Höhepunkt und fällt um Mitternacht ab. Ein 50%-iger Verkehrsanstieg mittags ist normal; um 3 Uhr morgens ist er verdächtig. Die saisonale Zerlegung trennt die Komponenten Trend, Saisonmuster und Residuum.
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 > thresholdAufbau einer Alerting-Pipeline
Anomalieerkennung ist nur nützlich, wenn sie umsetzbare Alerts erzeugt, ohne den Bereitschaftsdienst in Lärm zu ertränken.
// ❌ 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",
};
}
}Mehrere Signale kombinieren
Anomalieerkennung mit nur einer Metrik erzeugt False Positives. Die Kombination korrelierter Metriken verbessert die Genauigkeit drastisch.
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 → alertDie wichtigsten Erkenntnisse
- Statische Schwellenwerte brechen, wenn sich Verkehrsmuster ändern — ersetze "Alarm über X" durch dynamische Baselines, die Tageszeit- und Wochentagsmuster berücksichtigen
- Z-Scores sind der einfachste Anomaliedetektor — liegt ein Wert mehr als 3 Standardabweichungen vom Mittelwert entfernt, ist er ungewöhnlich; das erkennt 99.7% der echten Anomalien bei minimalen False Positives
- Fordere anhaltende Anomalien, bevor du alarmierst — ein einzelner anomaler Datenpunkt kann Rauschen sein; verlange 3+ aufeinanderfolgende anomale Punkte, um einen echten Vorfall zu bestätigen
- Kombiniere korrelierte Metriken, um False Positives zu reduzieren — hohe Fehlerrate plus hohe Latenz plus normaler Verkehr ist eher ein echter Vorfall als eine hohe Fehlerrate allein
- Saisonale Baselines verhindern tageszeitabhängige Fehlalarme — baue separate Baselines für jede Stunde und jeden Wochentag, um nicht auf normale tägliche Verkehrsmuster zu alarmieren


