test: Add CheckGuest tests (41.4%→97.4%)
Cover all CheckGuest branches: - Early return when alerts disabled - Early return when all guests disabled - VM and Container type handling - Unsupported guest type returns early - pulse-no-alerts tag suppresses alerts - Stopped guest triggers powered-off check - DisableAllGuestsOffline clears tracking - Paused guest clears powered-off alert - Non-running guest clears metric alerts - Running guest clears powered-off alert - Disabled thresholds clear existing alerts - CPU, memory, disk metric checks - Individual disk checks (mountpoint, device, index fallback) - Skips disk with zero total or negative usage - I/O metrics (diskRead, diskWrite, networkIn, networkOut) - pulse-relaxed tag applies relaxed thresholds Alerts package coverage: 76.0%→78.6%
This commit is contained in:
parent
4ceb4f9a35
commit
f4e3f625e7
1 changed files with 765 additions and 0 deletions
|
|
@ -11730,3 +11730,768 @@ func TestCheckNode(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckGuest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("returns early when alerts disabled", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = false
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
CPU: 0.95,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alertCount := len(m.activeAlerts)
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alertCount != 0 {
|
||||||
|
t.Errorf("expected no alerts when disabled, got %d", alertCount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns early when all guests disabled", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.DisableAllGuests = true
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
CPU: 0.95,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alertCount := len(m.activeAlerts)
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alertCount != 0 {
|
||||||
|
t.Errorf("expected no alerts when all guests disabled, got %d", alertCount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles VM type correctly", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
CPU: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
CPU: 0.95, // 95%
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["vm100-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected cpu alert for VM")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles Container type correctly", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
CPU: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
ct := models.Container{
|
||||||
|
ID: "ct101",
|
||||||
|
Name: "TestCT",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
CPU: 0.95, // 95%
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(ct, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["ct101-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected cpu alert for Container")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns for unsupported guest type", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
// Pass a string which is unsupported
|
||||||
|
m.CheckGuest("invalid", "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alertCount := len(m.activeAlerts)
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alertCount != 0 {
|
||||||
|
t.Errorf("expected no alerts for unsupported type, got %d", alertCount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("suppresses alerts with pulse-no-alerts tag", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
// Pre-create an alert
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["vm100-cpu"] = &Alert{
|
||||||
|
ID: "vm100-cpu",
|
||||||
|
ResourceID: "vm100",
|
||||||
|
Type: "cpu",
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
CPU: 0.95,
|
||||||
|
Tags: []string{"pulse-no-alerts"},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["vm100-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("expected alert to be suppressed with pulse-no-alerts tag")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stopped guest triggers powered-off check", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
// Pre-set confirmation count to trigger alert
|
||||||
|
m.mu.Lock()
|
||||||
|
m.offlineConfirmations["vm100"] = 2
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "stopped",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["guest-powered-off-vm100"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected powered-off alert for stopped guest")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stopped guest with DisableAllGuestsOffline clears tracking", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.DisableAllGuestsOffline = true
|
||||||
|
m.offlineConfirmations["vm100"] = 5
|
||||||
|
m.activeAlerts["guest-powered-off-vm100"] = &Alert{
|
||||||
|
ID: "guest-powered-off-vm100",
|
||||||
|
ResourceID: "vm100",
|
||||||
|
Type: "powered-off",
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "stopped",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, alertExists := m.activeAlerts["guest-powered-off-vm100"]
|
||||||
|
_, countExists := m.offlineConfirmations["vm100"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alertExists {
|
||||||
|
t.Error("expected powered-off alert to be cleared")
|
||||||
|
}
|
||||||
|
if countExists {
|
||||||
|
t.Error("expected offline count to be cleared")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("paused guest clears powered-off alert", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["guest-powered-off-vm100"] = &Alert{
|
||||||
|
ID: "guest-powered-off-vm100",
|
||||||
|
ResourceID: "vm100",
|
||||||
|
Type: "powered-off",
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "paused",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest-powered-off-vm100"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("expected powered-off alert to be cleared for paused guest")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-running guest clears metric alerts", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["vm100-cpu"] = &Alert{
|
||||||
|
ID: "vm100-cpu",
|
||||||
|
ResourceID: "vm100",
|
||||||
|
Type: "cpu",
|
||||||
|
}
|
||||||
|
m.activeAlerts["vm100-memory"] = &Alert{
|
||||||
|
ID: "vm100-memory",
|
||||||
|
ResourceID: "vm100",
|
||||||
|
Type: "memory",
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "stopped",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, cpuExists := m.activeAlerts["vm100-cpu"]
|
||||||
|
_, memExists := m.activeAlerts["vm100-memory"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if cpuExists {
|
||||||
|
t.Error("expected cpu alert to be cleared for non-running guest")
|
||||||
|
}
|
||||||
|
if memExists {
|
||||||
|
t.Error("expected memory alert to be cleared for non-running guest")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("running guest clears powered-off alert", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["guest-powered-off-vm100"] = &Alert{
|
||||||
|
ID: "guest-powered-off-vm100",
|
||||||
|
ResourceID: "vm100",
|
||||||
|
Type: "powered-off",
|
||||||
|
}
|
||||||
|
m.offlineConfirmations["vm100"] = 5
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest-powered-off-vm100"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("expected powered-off alert to be cleared for running guest")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disabled thresholds clear existing alerts", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["vm100-cpu"] = &Alert{
|
||||||
|
ID: "vm100-cpu",
|
||||||
|
ResourceID: "vm100",
|
||||||
|
Type: "cpu",
|
||||||
|
}
|
||||||
|
m.config.Overrides = map[string]ThresholdConfig{
|
||||||
|
"vm100": {Disabled: true},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["vm100-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("expected alert to be cleared when guest has alerts disabled")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks memory metric", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
Memory: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
Memory: models.Memory{Usage: 95.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["vm100-memory"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected memory alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks disk metric", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
Disk: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
Disk: models.Disk{Usage: 95.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["vm100-disk"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected disk alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks individual disks", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
Disk: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
Disks: []models.Disk{
|
||||||
|
{Mountpoint: "/", Usage: 95.0, Total: 100},
|
||||||
|
{Mountpoint: "/data", Usage: 50.0, Total: 100},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
// Check that alert for high disk was created
|
||||||
|
var foundDiskAlert bool
|
||||||
|
for alertID := range m.activeAlerts {
|
||||||
|
if strings.Contains(alertID, "vm100-disk-") {
|
||||||
|
foundDiskAlert = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !foundDiskAlert {
|
||||||
|
t.Fatal("expected individual disk alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips disk with zero total", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
Disk: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
Disks: []models.Disk{
|
||||||
|
{Mountpoint: "/", Usage: 95.0, Total: 0}, // Zero total - should skip
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
var foundDiskAlert bool
|
||||||
|
for alertID := range m.activeAlerts {
|
||||||
|
if strings.Contains(alertID, "vm100-disk-") {
|
||||||
|
foundDiskAlert = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if foundDiskAlert {
|
||||||
|
t.Error("expected no disk alert for disk with zero total")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips disk with negative usage", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
Disk: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
Disks: []models.Disk{
|
||||||
|
{Mountpoint: "/", Usage: -1.0, Total: 100}, // Negative usage - should skip
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
var foundDiskAlert bool
|
||||||
|
for alertID := range m.activeAlerts {
|
||||||
|
if strings.Contains(alertID, "vm100-disk-") {
|
||||||
|
foundDiskAlert = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if foundDiskAlert {
|
||||||
|
t.Error("expected no disk alert for disk with negative usage")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks diskRead metric", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
DiskRead: &HysteresisThreshold{Trigger: 100.0, Clear: 80.0}, // MB/s
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
DiskRead: 200 * 1024 * 1024, // 200 MB/s in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["vm100-diskRead"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected diskRead alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks diskWrite metric", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
DiskWrite: &HysteresisThreshold{Trigger: 100.0, Clear: 80.0}, // MB/s
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
DiskWrite: 200 * 1024 * 1024, // 200 MB/s in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["vm100-diskWrite"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected diskWrite alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks networkIn metric", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
NetworkIn: &HysteresisThreshold{Trigger: 100.0, Clear: 80.0}, // MB/s
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
NetworkIn: 200 * 1024 * 1024, // 200 MB/s in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["vm100-networkIn"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected networkIn alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("checks networkOut metric", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
NetworkOut: &HysteresisThreshold{Trigger: 100.0, Clear: 80.0}, // MB/s
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
NetworkOut: 200 * 1024 * 1024, // 200 MB/s in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert := m.activeAlerts["vm100-networkOut"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alert == nil {
|
||||||
|
t.Fatal("expected networkOut alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("applies relaxed thresholds with pulse-relaxed tag", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
CPU: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// CPU at 90% - would trigger normally but relaxed threshold is 95%
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
CPU: 0.90, // 90%
|
||||||
|
Tags: []string{"pulse-relaxed"},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["vm100-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("expected no alert due to relaxed thresholds")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disk uses device as label fallback", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
Disk: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
Disks: []models.Disk{
|
||||||
|
{Device: "sda1", Usage: 95.0, Total: 100}, // No mountpoint, has device
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
var foundDiskAlert bool
|
||||||
|
for alertID := range m.activeAlerts {
|
||||||
|
if strings.Contains(alertID, "vm100-disk-") {
|
||||||
|
foundDiskAlert = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !foundDiskAlert {
|
||||||
|
t.Fatal("expected disk alert using device as label")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disk uses index as label when no mountpoint or device", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.TimeThreshold = 0
|
||||||
|
m.config.TimeThresholds = map[string]int{}
|
||||||
|
m.config.GuestDefaults = ThresholdConfig{
|
||||||
|
Disk: &HysteresisThreshold{Trigger: 80.0, Clear: 70.0},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
vm := models.VM{
|
||||||
|
ID: "vm100",
|
||||||
|
Name: "TestVM",
|
||||||
|
Node: "node1",
|
||||||
|
Status: "running",
|
||||||
|
Disks: []models.Disk{
|
||||||
|
{Usage: 95.0, Total: 100}, // No mountpoint or device
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CheckGuest(vm, "pve1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
var foundDiskAlert bool
|
||||||
|
for alertID := range m.activeAlerts {
|
||||||
|
if strings.Contains(alertID, "vm100-disk-") {
|
||||||
|
foundDiskAlert = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !foundDiskAlert {
|
||||||
|
t.Fatal("expected disk alert using index as label")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue