Fix false-positive slow alerts: require 3 consecutive checks + use P95 baseline

Previously a single check above 2× average response time fired an alert.
This caused false positives from normal network variance.

- scheduler.js: track consecutive slow checks per resource; only alert after
  3 in a row (resets to 0 when response time normalises)
- monitorService.js: calculateResponseTimeBaseline now returns P95 instead
  of mean, making 2× baseline a much more realistic slow threshold

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-06 16:50:57 -04:00
1 parent 98e0949d08
commit dd85a79d55
2 files changed
+28 -19

No files matched your search

+4 -3
View File
@@ -765,9 +765,10 @@ class MonitorService {
return null; return null;
} }
// Return average of response times // Use P95 so the baseline reflects realistic worst-case, not the average.
const sum = checks.reduce((acc, c) => acc + (c.response_time || 0), 0); // Threshold is 2× P95, making it very unlikely to trigger on normal variance.
return Math.round(sum / checks.length); const p95Index = Math.min(Math.floor(checks.length * 0.95), checks.length - 1);
return Math.round(checks[p95Index].response_time);
} }
// Check if response time is anomalous // Check if response time is anomalous
+24 -16
View File
@@ -9,6 +9,7 @@ class Scheduler {
this.isRunning = false; this.isRunning = false;
this.lastCheckedAt = new Map(); this.lastCheckedAt = new Map();
this.slowAlertCooldown = new Map(); this.slowAlertCooldown = new Map();
this.slowConsecutiveCount = new Map(); // tracks consecutive slow checks per resource
} }
start() { start() {
@@ -172,26 +173,33 @@ class Scheduler {
pendingAlerts.push({ resource, incident, stats }); pendingAlerts.push({ resource, incident, stats });
} }
// Slow alert: prefer explicit threshold, fall back to 2× computed baseline // Slow alert: prefer explicit threshold, fall back to 2× P95 baseline
const effectiveThreshold = resource.response_time_threshold || const effectiveThreshold = resource.response_time_threshold ||
(resource.response_time_baseline ? resource.response_time_baseline * 2 : null); (resource.response_time_baseline ? resource.response_time_baseline * 2 : null);
if ( if (result.status === 'up' && effectiveThreshold && !resource.maintenance_mode) {
result.status === 'up' && if (result.response_time > effectiveThreshold) {
effectiveThreshold && const consecutive = (this.slowConsecutiveCount.get(resource.id) || 0) + 1;
result.response_time > effectiveThreshold && this.slowConsecutiveCount.set(resource.id, consecutive);
!resource.maintenance_mode // Require 3 consecutive slow checks before alerting (avoids single-spike false positives)
) { if (consecutive >= 3) {
const lastSlowAlert = this.slowAlertCooldown.get(resource.id) || 0; const lastSlowAlert = this.slowAlertCooldown.get(resource.id) || 0;
if (now - lastSlowAlert > 60 * 60 * 1000) { if (now - lastSlowAlert > 60 * 60 * 1000) {
this.slowAlertCooldown.set(resource.id, now); this.slowAlertCooldown.set(resource.id, now);
await notificationService.sendAlert(resource, { await notificationService.sendAlert(resource, {
type: 'slow', type: 'slow',
responseTime: result.response_time, responseTime: result.response_time,
threshold: effectiveThreshold, threshold: effectiveThreshold,
id: null, id: null,
}); });
}
}
} else {
// Reset consecutive counter when response time is back to normal
this.slowConsecutiveCount.set(resource.id, 0);
} }
} else if (!effectiveThreshold) {
this.slowConsecutiveCount.set(resource.id, 0);
} }
} catch (error) { } catch (error) {
// Check error handled silently // Check error handled silently