From 85fbbd4ef61110e0a6d51d2aaa8678aeb4061675 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 13 May 2026 06:32:07 +0100 Subject: [PATCH] consult DiskFillByType in host disk fill alert evaluation Inserts a per-type threshold lookup before the global AgentDefaults.Disk fallback in CheckHost's disk evaluation. inferDiskHardwareType maps the disk device path to "nvme" (only NVMe is inferable today); when the DiskFillByType map carries a matching key, that hysteresis threshold is used, otherwise evaluation falls through to the existing global default. Storage-type unified_eval branch is unchanged. Tests prove: (a) NVMe device at 91% does not alert when DiskFillByType sets the nvme trigger to 92; (b) the same device at 93% alerts at the nvme threshold; (c) /dev/sda1 falls back to the global threshold; (d) the storage-type alert branch is not regressed. --- internal/alerts/host.go | 12 ++ internal/alerts/host_disk_type_test.go | 169 +++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 internal/alerts/host_disk_type_test.go diff --git a/internal/alerts/host.go b/internal/alerts/host.go index 1917c8a73..fc9a3fe50 100644 --- a/internal/alerts/host.go +++ b/internal/alerts/host.go @@ -371,6 +371,18 @@ func (m *Manager) CheckHost(host models.Host) { effectiveDiskThreshold = ensureHysteresisThreshold(diskOverride.Disk) } } + // Per-type override: consult DiskFillByType if hardware type is inferable + // from the device path and no disk-specific override applied above. + if effectiveDiskThreshold == nil && thresholds.Disk != nil && thresholds.Disk.Trigger > 0 { + if hwType := inferDiskHardwareType(disk.Device); hwType != "" { + m.mu.RLock() + if th, ok := m.config.DiskFillByType[hwType]; ok { + t := th + effectiveDiskThreshold = &t + } + m.mu.RUnlock() + } + } // Fall back to host-level threshold if effectiveDiskThreshold == nil { effectiveDiskThreshold = thresholds.Disk diff --git a/internal/alerts/host_disk_type_test.go b/internal/alerts/host_disk_type_test.go new file mode 100644 index 000000000..ef35c3058 --- /dev/null +++ b/internal/alerts/host_disk_type_test.go @@ -0,0 +1,169 @@ +package alerts + +import ( + "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/models" +) + +// configureDiskTypeHostManager builds an alert manager configured with a +// per-type NVMe override (92/87) and a global agent disk default (90/85), +// with immediate alerting (no time threshold, no suppression). +func configureDiskTypeHostManager(t *testing.T) *Manager { + t.Helper() + + m := newTestManager(t) + cfg := AlertConfig{ + Enabled: true, + ActivationState: ActivationActive, + AgentDefaults: ThresholdConfig{ + Disk: &HysteresisThreshold{Trigger: 90, Clear: 85}, + }, + DiskFillByType: map[string]HysteresisThreshold{ + "nvme": {Trigger: 92, Clear: 87}, + "sata": {Trigger: 90, Clear: 85}, + "hdd": {Trigger: 85, Clear: 80}, + }, + Overrides: map[string]ThresholdConfig{}, + TimeThresholds: map[string]int{}, + SuppressionWindow: 0, + MinimumDelta: 0, + } + m.UpdateConfig(cfg) + + m.mu.Lock() + m.config.TimeThresholds = map[string]int{} + m.config.MetricTimeThresholds = nil + m.config.SuppressionWindow = 0 + m.config.MinimumDelta = 0 + m.mu.Unlock() + + m.ClearActiveAlerts() + return m +} + +func TestHostDiskFillUsesPerTypeThresholdForNVMe(t *testing.T) { + m := configureDiskTypeHostManager(t) + + hostBase := models.Host{ + ID: "host-nvme", + Hostname: "host-nvme", + Status: "online", + } + + // 91% fill on an NVMe device must NOT alert: nvme trigger is 92. + hostBelow := hostBase + hostBelow.Disks = []models.Disk{{ + Mountpoint: "/", + Device: "/dev/nvme0n1p1", + Usage: 91.0, + Total: 1000, + Used: 910, + Free: 90, + }} + m.CheckHost(hostBelow) + + diskResourceID, _ := hostDiskResourceID(hostBelow, hostBelow.Disks[0]) + trackingKey := canonicalMetricStateID(diskResourceID, "disk") + if _, exists := testLookupActiveAlert(t, m, trackingKey); exists { + t.Fatalf("expected no alert for nvme disk at 91%% (nvme trigger 92), got active: %v", alertKeys(m)) + } + m.mu.RLock() + if _, pending := m.pendingAlerts[trackingKey]; pending { + m.mu.RUnlock() + t.Fatalf("expected no pending alert for nvme disk at 91%%, but pendingAlerts has %q", trackingKey) + } + m.mu.RUnlock() + + // 93% fill on the same NVMe device must alert at the per-type threshold. + hostAbove := hostBase + hostAbove.Disks = []models.Disk{{ + Mountpoint: "/", + Device: "/dev/nvme0n1p1", + Usage: 93.0, + Total: 1000, + Used: 930, + Free: 70, + }} + m.CheckHost(hostAbove) + + if _, exists := testLookupActiveAlert(t, m, trackingKey); !exists { + t.Fatalf("expected alert for nvme disk at 93%% (nvme trigger 92), active: %v", alertKeys(m)) + } +} + +func TestHostDiskFillFallsBackToGlobalForUnknownDevice(t *testing.T) { + m := configureDiskTypeHostManager(t) + + host := models.Host{ + ID: "host-sata", + Hostname: "host-sata", + Status: "online", + Disks: []models.Disk{{ + Mountpoint: "/", + Device: "/dev/sda1", + Usage: 91.0, + Total: 1000, + Used: 910, + Free: 90, + }}, + } + + m.CheckHost(host) + + diskResourceID, _ := hostDiskResourceID(host, host.Disks[0]) + trackingKey := canonicalMetricStateID(diskResourceID, "disk") + if _, exists := testLookupActiveAlert(t, m, trackingKey); !exists { + t.Fatalf("expected alert for /dev/sda1 at 91%% (global trigger 90, sata key dormant), active: %v", alertKeys(m)) + } +} + +func TestHostDiskFillPerTypeThresholdDoesNotOverrideDisabledGlobalDefault(t *testing.T) { + m := configureDiskTypeHostManager(t) + + m.mu.Lock() + m.config.AgentDefaults.Disk = &HysteresisThreshold{Trigger: 0, Clear: 0} + m.mu.Unlock() + + host := models.Host{ + ID: "host-disabled-nvme", + Hostname: "host-disabled-nvme", + Status: "online", + Disks: []models.Disk{{ + Mountpoint: "/", + Device: "/dev/nvme0n1p1", + Usage: 93.0, + Total: 1000, + Used: 930, + Free: 70, + }}, + } + + m.CheckHost(host) + + diskResourceID, _ := hostDiskResourceID(host, host.Disks[0]) + trackingKey := canonicalMetricStateID(diskResourceID, "disk") + if _, exists := testLookupActiveAlert(t, m, trackingKey); exists { + t.Fatalf("expected no alert when global agent disk threshold is disabled, active: %v", alertKeys(m)) + } + m.mu.RLock() + if _, pending := m.pendingAlerts[trackingKey]; pending { + m.mu.RUnlock() + t.Fatalf("expected no pending alert when global agent disk threshold is disabled, but pendingAlerts has %q", trackingKey) + } + m.mu.RUnlock() +} + +func TestStorageTypeBranchNotRegressed(t *testing.T) { + m := newTestManager(t) + configureUnifiedEvalManager(t, m, unifiedEvalBaseConfig()) + + m.CheckUnifiedResource(&UnifiedResourceInput{ + ID: "storage-1", + Type: "storage", + Name: "storage-1", + Disk: &UnifiedResourceMetric{Percent: 92}, + }) + + assertAlertPresent(t, m, canonicalMetricStateID("storage-1", "usage")) +}