mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 04:38:48 +00:00
feat(alerts): resolve alert policy through one declarative fold
Phase 3 of docs/ALERT_ENGINE_EVOLUTION.md: the effective alert policy for a resource — type default thresholds, the type's DisableAll switches, custom rules, the per-resource override — is now answered by one ordered fold (effectiveAlertPolicyNoLock in alert_policy.go), translated from the persisted AlertConfig. The config keeps its shape; the engine stops reading it piecemeal. Before this, every check path read its own DisableAll* boolean and picked its own override lookup, and the scattered reads drifted (#1738 was an override lookup that existed on some paths and not others; connection.go hand-rolled its own type-to-switch mapping). Now: - resolveResourceThresholds and getGuestThresholds delegate to the fold. - All 40+ DisableAll* reads across the check paths, the config-change reconciliation, and the connection detector go through alertPolicyTypeSwitchesNoLock — the single place those booleans are read on behalf of evaluation. Characterization first: alert_policy_test.go pins the fold against the legacy resolution paths (per-type defaults, overrides, storage aliases, guest custom rules by priority, every type's switch pair) before any call site moved.
This commit is contained in:
parent
90cd0a57af
commit
8827defb63
15 changed files with 378 additions and 97 deletions
130
internal/alerts/alert_policy.go
Normal file
130
internal/alerts/alert_policy.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
package alerts
|
||||
|
||||
// Alert policy resolution: the declarative rule model scoped by
|
||||
// docs/ALERT_ENGINE_EVOLUTION.md, Phase 3. The persisted AlertConfig keeps
|
||||
// its shape — per-type default blocks, per-resource overrides, custom
|
||||
// rules, DisableAll* switches — and this file is the translator: one
|
||||
// ordered fold from that config to the effective policy for one resource.
|
||||
//
|
||||
// The point is a single resolution surface. Before this, every check path
|
||||
// read its own DisableAll* boolean and picked its own override lookup, and
|
||||
// the scattered reads drifted (#1738: overrides saved under registry IDs
|
||||
// silently never applying on legacy-ID paths; connection.go hand-rolling
|
||||
// its own type→switch mapping). Callers now ask one question — "what is
|
||||
// the effective alert policy for this resource?" — and the tiers are:
|
||||
//
|
||||
// 1. the type's default threshold block
|
||||
// 2. the type's DisableAll switches (all alerts / offline family)
|
||||
// 3. custom rules (guest kinds, live-filter scoped, priority order)
|
||||
// 4. the per-resource override, through the identity-aware lookup for
|
||||
// the kind (guest stable keys, storage aliases, canonical registry
|
||||
// translation)
|
||||
|
||||
// alertPolicyQuery identifies one resource asking for its effective
|
||||
// alert policy.
|
||||
type alertPolicyQuery struct {
|
||||
// TypeKey is the canonical resource type key ("vm", "node", "agent",
|
||||
// "storage", "pbs", "pmg", "docker-host", "k8s-node", ...).
|
||||
TypeKey string
|
||||
ResourceID string
|
||||
// Guest carries the live guest model when the query is for a guest
|
||||
// kind: filter-scoped custom rules and the clustered override
|
||||
// identity need it. Nil is valid — filter rules then never match and
|
||||
// override lookup falls back to the raw resource ID.
|
||||
Guest any
|
||||
// StorageAliases are the storage resource's alias IDs, honored by the
|
||||
// storage override lookup.
|
||||
StorageAliases []string
|
||||
}
|
||||
|
||||
// EffectiveAlertPolicy is the resolved policy for one resource.
|
||||
type EffectiveAlertPolicy struct {
|
||||
// AllDisabled reports the type's every-alert switch
|
||||
// (DisableAllNodes, DisableAllGuests, ...).
|
||||
AllDisabled bool
|
||||
// OfflineDisabled reports the type's connectivity/powered-state
|
||||
// family switch (DisableAllNodesOffline, ...). Per-resource
|
||||
// connectivity opt-outs are Thresholds.DisableConnectivity.
|
||||
OfflineDisabled bool
|
||||
// Thresholds is the folded threshold block: type defaults, then
|
||||
// custom rules, then the per-resource override.
|
||||
Thresholds ThresholdConfig
|
||||
}
|
||||
|
||||
// alertPolicyTypeSwitches maps a resource type key to its global disable
|
||||
// switches. This is the one place the DisableAll* booleans are read on
|
||||
// behalf of evaluation paths.
|
||||
func (m *Manager) alertPolicyTypeSwitchesNoLock(typeKey string) (allDisabled, offlineDisabled bool) {
|
||||
switch typeKey {
|
||||
case "vm", "system-container":
|
||||
return m.config.DisableAllGuests, m.config.DisableAllGuestsOffline
|
||||
case "app-container":
|
||||
return m.config.DisableAllDockerContainers, false
|
||||
case "docker-service":
|
||||
return m.config.DisableAllDockerServices, false
|
||||
case "docker-host":
|
||||
return m.config.DisableAllDockerHosts, m.config.DisableAllDockerHostsOffline
|
||||
case "node":
|
||||
return m.config.DisableAllNodes, m.config.DisableAllNodesOffline
|
||||
case "agent":
|
||||
return m.config.DisableAllAgents, m.config.DisableAllAgentsOffline
|
||||
case "storage":
|
||||
return m.config.DisableAllStorage, false
|
||||
case "pbs":
|
||||
return m.config.DisableAllPBS, m.config.DisableAllPBSOffline
|
||||
case "pmg":
|
||||
return m.config.DisableAllPMG, m.config.DisableAllPMGOffline
|
||||
default:
|
||||
if isUnifiedModernPlatformAlertType(typeKey) {
|
||||
return m.unifiedPlatformAlertsDisabledNoLock(typeKey), false
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
// customRuleThresholdsNoLock folds the highest-priority matching enabled
|
||||
// custom rule onto base. Custom rules are guest-scoped: they match on
|
||||
// live guest state, so a query without a guest never matches one.
|
||||
func (m *Manager) customRuleThresholdsNoLock(base ThresholdConfig, guest any) ThresholdConfig {
|
||||
if guest == nil {
|
||||
return base
|
||||
}
|
||||
var applicable *CustomAlertRule
|
||||
highestPriority := -1
|
||||
for i := range m.config.CustomRules {
|
||||
rule := &m.config.CustomRules[i]
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
if m.evaluateFilterStack(guest, rule.FilterConditions) {
|
||||
if rule.Priority > highestPriority {
|
||||
applicable = rule
|
||||
highestPriority = rule.Priority
|
||||
}
|
||||
}
|
||||
}
|
||||
if applicable == nil {
|
||||
return base
|
||||
}
|
||||
return m.applyThresholdOverride(base, applicable.Thresholds)
|
||||
}
|
||||
|
||||
// effectiveAlertPolicyNoLock resolves the alert policy for one resource.
|
||||
// Callers must hold m.mu.
|
||||
func (m *Manager) effectiveAlertPolicyNoLock(q alertPolicyQuery) EffectiveAlertPolicy {
|
||||
policy := EffectiveAlertPolicy{}
|
||||
policy.AllDisabled, policy.OfflineDisabled = m.alertPolicyTypeSwitchesNoLock(q.TypeKey)
|
||||
|
||||
thresholds := m.defaultThresholdsForResourceType(q.TypeKey)
|
||||
switch {
|
||||
case isGuestThresholdResourceType(q.TypeKey):
|
||||
thresholds = m.customRuleThresholdsNoLock(thresholds, q.Guest)
|
||||
thresholds = m.resolveGuestThresholdOverride(thresholds, q.Guest, q.ResourceID)
|
||||
case q.TypeKey == "storage":
|
||||
thresholds = m.resolveStorageThresholdOverride(thresholds, q.ResourceID, q.StorageAliases)
|
||||
default:
|
||||
thresholds = m.resolveThresholdOverride(thresholds, q.ResourceID)
|
||||
}
|
||||
policy.Thresholds = thresholds
|
||||
return policy
|
||||
}
|
||||
193
internal/alerts/alert_policy_test.go
Normal file
193
internal/alerts/alert_policy_test.go
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package alerts
|
||||
|
||||
// Characterization for the Phase 3 policy resolution
|
||||
// (docs/ALERT_ENGINE_EVOLUTION.md): effectiveAlertPolicyNoLock must answer
|
||||
// exactly what the scattered legacy reads answered — the per-type
|
||||
// resolveResourceThresholds / getGuestThresholds paths and the DisableAll*
|
||||
// booleans — across the config surface. Any divergence is a bug in the
|
||||
// fold, never an improvement.
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
)
|
||||
|
||||
func testPolicyConfigMatrix() AlertConfig {
|
||||
trig := func(t, c float64) *HysteresisThreshold {
|
||||
return &HysteresisThreshold{Trigger: t, Clear: c}
|
||||
}
|
||||
return AlertConfig{
|
||||
Enabled: true,
|
||||
GuestDefaults: ThresholdConfig{CPU: trig(80, 75), Memory: trig(85, 80)},
|
||||
NodeDefaults: ThresholdConfig{CPU: trig(85, 80), Temperature: trig(75, 70)},
|
||||
AgentDefaults: ThresholdConfig{CPU: trig(90, 85), DiskTemperature: trig(60, 55)},
|
||||
PBSDefaults: ThresholdConfig{CPU: trig(70, 65)},
|
||||
StorageDefault: HysteresisThreshold{Trigger: 88, Clear: 83},
|
||||
KubernetesDefaults: ThresholdConfig{
|
||||
CPU: trig(75, 70), Memory: trig(80, 75),
|
||||
},
|
||||
TrueNASDefaults: ThresholdConfig{CPU: trig(65, 60), Usage: trig(82, 77)},
|
||||
TrueNASDiskDefaults: ThresholdConfig{DiskTemperature: trig(55, 50)},
|
||||
VMwareDefaults: ThresholdConfig{CPU: trig(78, 73), Usage: trig(84, 79)},
|
||||
Overrides: map[string]ThresholdConfig{
|
||||
"node-2": {CPU: trig(95, 90)},
|
||||
"pbs-quiet": {Disabled: true},
|
||||
"storage-full": {Usage: trig(97, 94)},
|
||||
"storage-alias": {
|
||||
Usage: trig(93, 91),
|
||||
},
|
||||
"vm-override": {Memory: trig(99, 97), DisableConnectivity: true},
|
||||
},
|
||||
CustomRules: []CustomAlertRule{
|
||||
{
|
||||
Name: "named-guests",
|
||||
Enabled: true,
|
||||
Priority: 10,
|
||||
FilterConditions: FilterStack{
|
||||
LogicalOperator: "AND",
|
||||
Filters: []FilterCondition{
|
||||
{Type: "text", Field: "name", Value: "web"},
|
||||
},
|
||||
},
|
||||
Thresholds: ThresholdConfig{CPU: trig(60, 55)},
|
||||
},
|
||||
{
|
||||
Name: "disabled-rule",
|
||||
Enabled: false,
|
||||
Priority: 99,
|
||||
FilterConditions: FilterStack{
|
||||
LogicalOperator: "AND",
|
||||
Filters: []FilterCondition{
|
||||
{Type: "text", Field: "name", Value: "web"},
|
||||
},
|
||||
},
|
||||
Thresholds: ThresholdConfig{CPU: trig(10, 5)},
|
||||
},
|
||||
},
|
||||
DisableAllNodes: true,
|
||||
DisableAllGuestsOffline: true,
|
||||
DisableAllPBS: true,
|
||||
DisableAllPBSOffline: true,
|
||||
DisableAllDockerHostsOffline: true,
|
||||
DisableAllTrueNAS: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveAlertPolicyMatchesLegacyThresholdResolution(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
m.mu.Lock()
|
||||
m.config = testPolicyConfigMatrix()
|
||||
m.mu.Unlock()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
query alertPolicyQuery
|
||||
}{
|
||||
{"node default", alertPolicyQuery{TypeKey: "node", ResourceID: "node-1"}},
|
||||
{"node override", alertPolicyQuery{TypeKey: "node", ResourceID: "node-2"}},
|
||||
{"agent", alertPolicyQuery{TypeKey: "agent", ResourceID: "agent:host-1"}},
|
||||
{"pbs disabled override", alertPolicyQuery{TypeKey: "pbs", ResourceID: "pbs-quiet"}},
|
||||
{"storage default", alertPolicyQuery{TypeKey: "storage", ResourceID: "storage-empty"}},
|
||||
{"storage override", alertPolicyQuery{TypeKey: "storage", ResourceID: "storage-full"}},
|
||||
{"k8s node", alertPolicyQuery{TypeKey: "k8s-node", ResourceID: "k8s-node-1"}},
|
||||
{"truenas pool", alertPolicyQuery{TypeKey: "truenas-pool", ResourceID: "pool-1"}},
|
||||
{"vmware datastore", alertPolicyQuery{TypeKey: "vmware-datastore", ResourceID: "ds-1"}},
|
||||
{"vm without live guest", alertPolicyQuery{TypeKey: "vm", ResourceID: "vm-override"}},
|
||||
{"unknown type", alertPolicyQuery{TypeKey: "mystery", ResourceID: "x"}},
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
legacy := m.resolveResourceThresholds(tc.query.TypeKey, tc.query.ResourceID)
|
||||
policy := m.effectiveAlertPolicyNoLock(tc.query)
|
||||
if !reflect.DeepEqual(policy.Thresholds, legacy) {
|
||||
t.Fatalf("thresholds diverge from legacy resolution:\n policy: %+v\n legacy: %+v", policy.Thresholds, legacy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveAlertPolicyMatchesGuestResolutionWithCustomRules(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
m.mu.Lock()
|
||||
m.config = testPolicyConfigMatrix()
|
||||
m.mu.Unlock()
|
||||
|
||||
guests := []struct {
|
||||
name string
|
||||
guest models.VM
|
||||
id string
|
||||
}{
|
||||
{"custom rule matches", models.VM{Name: "web-1", Node: "pve1", Instance: "pve1", VMID: 100}, "pve1-100"},
|
||||
{"custom rule misses", models.VM{Name: "db-1", Node: "pve1", Instance: "pve1", VMID: 101}, "pve1-101"},
|
||||
{"override wins over rule", models.VM{Name: "web-2", Node: "pve1", Instance: "pve1", VMID: 102}, "vm-override"},
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for _, tc := range guests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
legacy := m.getGuestThresholds(tc.guest, tc.id)
|
||||
policy := m.effectiveAlertPolicyNoLock(alertPolicyQuery{TypeKey: "vm", ResourceID: tc.id, Guest: tc.guest})
|
||||
if !reflect.DeepEqual(policy.Thresholds, legacy) {
|
||||
t.Fatalf("guest thresholds diverge from legacy resolution:\n policy: %+v\n legacy: %+v", policy.Thresholds, legacy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveAlertPolicyMatchesStorageAliasResolution(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
m.mu.Lock()
|
||||
m.config = testPolicyConfigMatrix()
|
||||
m.mu.Unlock()
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
legacy := m.resolveStorageThresholdOverride(m.defaultThresholdsForResourceType("storage"), "storage-unknown", []string{"storage-alias"})
|
||||
policy := m.effectiveAlertPolicyNoLock(alertPolicyQuery{TypeKey: "storage", ResourceID: "storage-unknown", StorageAliases: []string{"storage-alias"}})
|
||||
if !reflect.DeepEqual(policy.Thresholds, legacy) {
|
||||
t.Fatalf("alias storage thresholds diverge:\n policy: %+v\n legacy: %+v", policy.Thresholds, legacy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveAlertPolicySwitchesMatchLegacyBooleans(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
m.mu.Lock()
|
||||
m.config = testPolicyConfigMatrix()
|
||||
m.mu.Unlock()
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
cases := []struct {
|
||||
typeKey string
|
||||
wantAll bool
|
||||
wantOffline bool
|
||||
}{
|
||||
{"node", m.config.DisableAllNodes, m.config.DisableAllNodesOffline},
|
||||
{"vm", m.config.DisableAllGuests, m.config.DisableAllGuestsOffline},
|
||||
{"system-container", m.config.DisableAllGuests, m.config.DisableAllGuestsOffline},
|
||||
{"app-container", m.config.DisableAllDockerContainers, false},
|
||||
{"docker-host", m.config.DisableAllDockerHosts, m.config.DisableAllDockerHostsOffline},
|
||||
{"docker-service", m.config.DisableAllDockerServices, false},
|
||||
{"agent", m.config.DisableAllAgents, m.config.DisableAllAgentsOffline},
|
||||
{"storage", m.config.DisableAllStorage, false},
|
||||
{"pbs", m.config.DisableAllPBS, m.config.DisableAllPBSOffline},
|
||||
{"pmg", m.config.DisableAllPMG, m.config.DisableAllPMGOffline},
|
||||
{"k8s-node", m.config.DisableAllKubernetes, false},
|
||||
{"truenas-pool", m.config.DisableAllTrueNAS, false},
|
||||
{"vmware-vm", m.config.DisableAllVMware, false},
|
||||
{"mystery", false, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
policy := m.effectiveAlertPolicyNoLock(alertPolicyQuery{TypeKey: tc.typeKey, ResourceID: "r"})
|
||||
if policy.AllDisabled != tc.wantAll || policy.OfflineDisabled != tc.wantOffline {
|
||||
t.Errorf("%s: switches = (all=%v offline=%v), want (all=%v offline=%v)",
|
||||
tc.typeKey, policy.AllDisabled, policy.OfflineDisabled, tc.wantAll, tc.wantOffline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,7 +154,17 @@ func normalizeOverrides(overrides map[string]ThresholdConfig) {
|
|||
// applyGlobalOfflineSettingsLocked clears tracking and active alerts for globally disabled offline detectors.
|
||||
// Caller must hold m.mu.
|
||||
func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
||||
if m.config.DisableAllNodesOffline {
|
||||
_, nodesOfflineDisabled := m.alertPolicyTypeSwitchesNoLock("node")
|
||||
_, pbsOfflineDisabled := m.alertPolicyTypeSwitchesNoLock("pbs")
|
||||
_, guestsOfflineDisabled := m.alertPolicyTypeSwitchesNoLock("vm")
|
||||
_, dockerHostsOfflineDisabled := m.alertPolicyTypeSwitchesNoLock("docker-host")
|
||||
containersDisabled, _ := m.alertPolicyTypeSwitchesNoLock("app-container")
|
||||
servicesDisabled, _ := m.alertPolicyTypeSwitchesNoLock("docker-service")
|
||||
kubernetesDisabled, _ := m.alertPolicyTypeSwitchesNoLock("k8s-node")
|
||||
truenasDisabled, _ := m.alertPolicyTypeSwitchesNoLock("truenas-system")
|
||||
vmwareDisabled, _ := m.alertPolicyTypeSwitchesNoLock("vmware-host")
|
||||
|
||||
if nodesOfflineDisabled {
|
||||
var nodeAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
if alert == nil {
|
||||
|
|
@ -172,7 +182,7 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
}
|
||||
}
|
||||
|
||||
if m.config.DisableAllPBSOffline {
|
||||
if pbsOfflineDisabled {
|
||||
var pbsAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
if alert != nil && alert.CanonicalKind == string(alertspecs.AlertSpecKindConnectivity) {
|
||||
|
|
@ -186,7 +196,7 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
}
|
||||
}
|
||||
|
||||
if m.config.DisableAllGuestsOffline {
|
||||
if guestsOfflineDisabled {
|
||||
var guestAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
if alert != nil && alert.CanonicalKind == string(alertspecs.AlertSpecKindPoweredState) {
|
||||
|
|
@ -198,7 +208,7 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
}
|
||||
}
|
||||
|
||||
if m.config.DisableAllDockerHostsOffline {
|
||||
if dockerHostsOfflineDisabled {
|
||||
var hostAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
if alert != nil && alert.CanonicalKind == string(alertspecs.AlertSpecKindConnectivity) {
|
||||
|
|
@ -212,7 +222,7 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
}
|
||||
}
|
||||
|
||||
if m.config.DisableAllDockerContainers {
|
||||
if containersDisabled {
|
||||
var containerAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
id := effectiveAlertID(alert, storageKey)
|
||||
|
|
@ -227,12 +237,12 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
m.dockerUpdateFirstSeen = make(map[string]time.Time)
|
||||
m.dockerUpdateFirstSeenByIdentity = make(map[string]time.Time)
|
||||
}
|
||||
if m.config.DockerDefaults.UpdateAlertDelayHours < 0 && !m.config.DisableAllDockerContainers {
|
||||
if m.config.DockerDefaults.UpdateAlertDelayHours < 0 && !containersDisabled {
|
||||
m.clearDockerContainerUpdateAlertsLocked()
|
||||
m.dockerUpdateFirstSeen = make(map[string]time.Time)
|
||||
m.dockerUpdateFirstSeenByIdentity = make(map[string]time.Time)
|
||||
}
|
||||
if m.config.DisableAllDockerServices {
|
||||
if servicesDisabled {
|
||||
var serviceAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
id := effectiveAlertID(alert, storageKey)
|
||||
|
|
@ -245,7 +255,7 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
}
|
||||
}
|
||||
|
||||
if m.config.DisableAllKubernetes || m.config.DisableAllTrueNAS || m.config.DisableAllVMware {
|
||||
if kubernetesDisabled || truenasDisabled || vmwareDisabled {
|
||||
var platformAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
primaryType := alertPrimaryResourceType(alert)
|
||||
|
|
@ -253,11 +263,11 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
continue
|
||||
}
|
||||
switch {
|
||||
case m.config.DisableAllKubernetes && isUnifiedKubernetesAlertType(primaryType):
|
||||
case kubernetesDisabled && isUnifiedKubernetesAlertType(primaryType):
|
||||
platformAlerts = append(platformAlerts, effectiveAlertID(alert, storageKey))
|
||||
case m.config.DisableAllTrueNAS && isUnifiedTrueNASAlertType(primaryType):
|
||||
case truenasDisabled && isUnifiedTrueNASAlertType(primaryType):
|
||||
platformAlerts = append(platformAlerts, effectiveAlertID(alert, storageKey))
|
||||
case m.config.DisableAllVMware && isUnifiedVMwareAlertType(primaryType):
|
||||
case vmwareDisabled && isUnifiedVMwareAlertType(primaryType):
|
||||
platformAlerts = append(platformAlerts, effectiveAlertID(alert, storageKey))
|
||||
}
|
||||
}
|
||||
|
|
@ -381,7 +391,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
}
|
||||
|
||||
if alert.Type == "queue-depth" || alert.Type == "queue-deferred" || alert.Type == "queue-hold" || alert.Type == "message-age" {
|
||||
if m.config.DisableAllPMG {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("pmg"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
|
|
@ -404,7 +414,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
|
||||
isAgentResource := alertResourceTypeKeysContain(resourceTypeKeys, "agent")
|
||||
if !handledModernPlatformType && isAgentResource {
|
||||
if m.config.DisableAllAgents {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("agent"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
|
|
@ -426,14 +436,14 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
}
|
||||
|
||||
if resourceTypeMeta == "docker-host" {
|
||||
if m.config.DisableAllDockerHosts {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("docker-host"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
if resourceTypeMeta == "app-container" {
|
||||
if m.config.DisableAllDockerContainers {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("app-container"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
|
|
@ -473,7 +483,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
isNodeResource := primaryResourceType == "" || primaryResourceType == "node"
|
||||
isStorageResource := alertResourceTypeKeysContain(resourceTypeKeys, "storage")
|
||||
if threshold == nil && !handledModernPlatformType && isNodeResource && !strings.Contains(resourceID, ":") && (alert.Instance == "Node" || alert.Instance == alert.Node) {
|
||||
if m.config.DisableAllNodes {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("node"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
|
|
@ -484,7 +494,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
}
|
||||
threshold = getThresholdForMetric(thresholds, metricType)
|
||||
} else if threshold == nil && !handledModernPlatformType && (isStorageResource || alert.Instance == "Storage" || strings.Contains(alert.ResourceID, ":storage/")) {
|
||||
if m.config.DisableAllStorage {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("storage"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
|
|
@ -495,7 +505,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
}
|
||||
threshold = getThresholdForMetric(thresholds, metricType)
|
||||
} else if threshold == nil && !handledModernPlatformType && (resourceTypeMeta == "pbs" || alert.Instance == "PBS") {
|
||||
if m.config.DisableAllPBS {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("pbs"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
|
|
@ -508,7 +518,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
}
|
||||
|
||||
if threshold == nil && !handledModernPlatformType {
|
||||
if m.config.DisableAllGuests {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("vm"); allDisabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,33 +97,21 @@ func (m *Manager) connectionDegradedPolicyDisabledNoLock(resourceID, policyResou
|
|||
thresholdType := ""
|
||||
switch connectionType {
|
||||
case ConnectionTypePVE:
|
||||
if m.config.DisableAllNodes || m.config.DisableAllNodesOffline {
|
||||
return true
|
||||
}
|
||||
thresholdType = "node"
|
||||
case ConnectionTypePBS:
|
||||
if m.config.DisableAllPBS || m.config.DisableAllPBSOffline {
|
||||
return true
|
||||
}
|
||||
thresholdType = "pbs"
|
||||
case ConnectionTypePMG:
|
||||
if m.config.DisableAllPMG || m.config.DisableAllPMGOffline {
|
||||
return true
|
||||
}
|
||||
thresholdType = "pmg"
|
||||
case ConnectionTypeVMware:
|
||||
if m.config.DisableAllVMware {
|
||||
return true
|
||||
}
|
||||
thresholdType = "vmware-host"
|
||||
case ConnectionTypeTrueNAS:
|
||||
if m.config.DisableAllTrueNAS {
|
||||
return true
|
||||
}
|
||||
thresholdType = "truenas-system"
|
||||
default:
|
||||
return true
|
||||
}
|
||||
if allDisabled, offlineDisabled := m.alertPolicyTypeSwitchesNoLock(thresholdType); allDisabled || offlineDisabled {
|
||||
return true
|
||||
}
|
||||
|
||||
policyResourceID = strings.TrimSpace(policyResourceID)
|
||||
if policyResourceID == "" {
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ func (m *Manager) CheckDockerHost(host models.DockerHost) {
|
|||
|
||||
m.mu.RLock()
|
||||
alertsEnabled := m.config.Enabled
|
||||
disableAllDockerHosts := m.config.DisableAllDockerHosts
|
||||
disableAllDockerHosts, _ := m.alertPolicyTypeSwitchesNoLock("docker-host")
|
||||
ignoredPrefixes := append([]string(nil), m.config.DockerIgnoredContainerPrefixes...)
|
||||
m.mu.RUnlock()
|
||||
if !alertsEnabled {
|
||||
|
|
@ -310,7 +310,7 @@ func (m *Manager) CheckDockerHost(host models.DockerHost) {
|
|||
|
||||
func (m *Manager) evaluateDockerContainer(host models.DockerHost, container models.DockerContainer, resourceID string) {
|
||||
m.mu.RLock()
|
||||
disableAllContainers := m.config.DisableAllDockerContainers
|
||||
disableAllContainers, _ := m.alertPolicyTypeSwitchesNoLock("app-container")
|
||||
m.mu.RUnlock()
|
||||
if disableAllContainers {
|
||||
return
|
||||
|
|
@ -475,7 +475,7 @@ func (m *Manager) evaluateDockerContainer(host models.DockerHost, container mode
|
|||
|
||||
func (m *Manager) evaluateDockerService(host models.DockerHost, service models.DockerService, resourceID string) {
|
||||
m.mu.RLock()
|
||||
disableAllServices := m.config.DisableAllDockerServices
|
||||
disableAllServices, _ := m.alertPolicyTypeSwitchesNoLock("docker-service")
|
||||
warnPct := m.config.DockerDefaults.ServiceWarnGapPct
|
||||
critPct := m.config.DockerDefaults.ServiceCritGapPct
|
||||
overrideConfig, hasOverride := m.config.Overrides[resourceID]
|
||||
|
|
@ -694,7 +694,7 @@ func (m *Manager) HandleDockerHostOffline(host models.DockerHost) {
|
|||
m.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
disableDockerHostsOffline := m.config.DisableAllDockerHostsOffline
|
||||
_, disableDockerHostsOffline := m.alertPolicyTypeSwitchesNoLock("docker-host")
|
||||
m.mu.RUnlock()
|
||||
|
||||
resourceID := fmt.Sprintf("docker:%s", strings.TrimSpace(host.ID))
|
||||
|
|
@ -1280,7 +1280,7 @@ func (m *Manager) shouldResolveDockerContainerUpdateAlertLocked(alert *Alert) bo
|
|||
return false
|
||||
}
|
||||
|
||||
if m.config.DisableAllDockerContainers || m.config.DockerDefaults.UpdateAlertDelayHours < 0 {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("app-container"); allDisabled || m.config.DockerDefaults.UpdateAlertDelayHours < 0 {
|
||||
m.clearDockerContainerUpdateStateLocked(alert)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ func (m *Manager) SyncExternalProbes(snapshots []ExternalProbeSnapshot) {
|
|||
func (m *Manager) checkExternalProbe(snapshot ExternalProbeSnapshot) {
|
||||
m.mu.RLock()
|
||||
enabled := m.config.Enabled
|
||||
disabled := m.config.DisableAllAgentsOffline
|
||||
_, disabled := m.alertPolicyTypeSwitchesNoLock("agent")
|
||||
thresholds := m.resolveHostThresholdsNoLock(snapshot.AgentID, "", "", "")
|
||||
m.mu.RUnlock()
|
||||
if !enabled || disabled || thresholds.Disabled || thresholds.DisableConnectivity {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// evaluateFilterCondition evaluates a single filter condition against a guest
|
||||
|
|
@ -211,37 +210,5 @@ func (m *Manager) evaluateFilterStack(guest any, stack FilterStack) bool {
|
|||
// getGuestThresholds returns the appropriate thresholds for a guest
|
||||
// Priority: Guest-specific overrides > Custom rules (by priority) > Global defaults
|
||||
func (m *Manager) getGuestThresholds(guest any, guestID string) ThresholdConfig {
|
||||
thresholds := cloneThresholdConfig(m.config.GuestDefaults)
|
||||
|
||||
// Check custom rules (sorted by priority, highest first)
|
||||
var applicableRule *CustomAlertRule
|
||||
highestPriority := -1
|
||||
|
||||
for i := range m.config.CustomRules {
|
||||
rule := &m.config.CustomRules[i]
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this rule applies to the guest
|
||||
if m.evaluateFilterStack(guest, rule.FilterConditions) {
|
||||
if rule.Priority > highestPriority {
|
||||
applicableRule = rule
|
||||
highestPriority = rule.Priority
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom rule thresholds if found
|
||||
if applicableRule != nil {
|
||||
thresholds = m.applyThresholdOverride(thresholds, applicableRule.Thresholds)
|
||||
|
||||
log.Debug().
|
||||
Str("guest", guestID).
|
||||
Str("rule", applicableRule.Name).
|
||||
Int("priority", applicableRule.Priority).
|
||||
Msg("Applied custom alert rule")
|
||||
}
|
||||
|
||||
return m.resolveGuestThresholdOverride(thresholds, guest, guestID)
|
||||
return m.effectiveAlertPolicyNoLock(alertPolicyQuery{TypeKey: "vm", ResourceID: guestID, Guest: guest}).Thresholds
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,7 @@ func (m *Manager) ReevaluateGuestAlert(guest any, guestID string) {
|
|||
func (m *Manager) CheckGuest(guest any, instanceName string) {
|
||||
m.mu.RLock()
|
||||
enabled := m.config.Enabled
|
||||
disableAllGuests := m.config.DisableAllGuests
|
||||
disableAllGuestsOffline := m.config.DisableAllGuestsOffline
|
||||
disableAllGuests, disableAllGuestsOffline := m.alertPolicyTypeSwitchesNoLock("vm")
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !enabled {
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ func (m *Manager) CheckHost(host models.Host) {
|
|||
|
||||
m.mu.RLock()
|
||||
alertsEnabled := m.config.Enabled
|
||||
disableAllAgents := m.config.DisableAllAgents
|
||||
disableAllAgents, _ := m.alertPolicyTypeSwitchesNoLock("agent")
|
||||
thresholds := m.resolveHostThresholdsNoLock(host.ID, host.LinkedNodeID, host.LinkedVMID, host.LinkedContainerID)
|
||||
// An explicit disk temperature override (host or inherited linked-resource)
|
||||
// beats the per-type defaults in DiskTempByType.
|
||||
|
|
@ -648,7 +648,7 @@ func (m *Manager) HandleHostTelemetryExpired(host models.Host) {
|
|||
|
||||
m.mu.RLock()
|
||||
alertsEnabled := m.config.Enabled
|
||||
disableAllAgents := m.config.DisableAllAgents
|
||||
disableAllAgents, _ := m.alertPolicyTypeSwitchesNoLock("agent")
|
||||
thresholds := m.resolveHostThresholdsNoLock(host.ID, host.LinkedNodeID, host.LinkedVMID, host.LinkedContainerID)
|
||||
m.mu.RUnlock()
|
||||
if !alertsEnabled || disableAllAgents || thresholds.Disabled {
|
||||
|
|
@ -726,7 +726,7 @@ func (m *Manager) HandleHostOffline(host models.Host) {
|
|||
m.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
disableHostsOffline := m.config.DisableAllAgentsOffline
|
||||
_, disableHostsOffline := m.alertPolicyTypeSwitchesNoLock("agent")
|
||||
thresholds := m.resolveHostThresholdsNoLock(host.ID, host.LinkedNodeID, host.LinkedVMID, host.LinkedContainerID)
|
||||
m.mu.RUnlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ func (m *Manager) CheckNode(node models.Node) {
|
|||
m.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
if m.config.DisableAllNodes {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("node"); allDisabled {
|
||||
m.mu.RUnlock()
|
||||
// Clear any existing node alerts when all node alerts are disabled
|
||||
m.mu.Lock()
|
||||
|
|
@ -53,7 +53,7 @@ func (m *Manager) CheckNode(node models.Node) {
|
|||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
disableNodesOffline := m.config.DisableAllNodesOffline
|
||||
_, disableNodesOffline := m.alertPolicyTypeSwitchesNoLock("node")
|
||||
thresholds := m.resolveResourceThresholds("node", node.ID)
|
||||
m.mu.RUnlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func (m *Manager) CheckPBS(pbs models.PBSInstance) {
|
|||
m.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
if m.config.DisableAllPBS {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("pbs"); allDisabled {
|
||||
m.mu.RUnlock()
|
||||
// Clear any existing PBS alerts when all PBS alerts are disabled
|
||||
m.mu.Lock()
|
||||
|
|
@ -67,7 +67,7 @@ func (m *Manager) CheckPBS(pbs models.PBSInstance) {
|
|||
}
|
||||
|
||||
thresholds := m.resolveResourceThresholds("pbs", pbs.ID)
|
||||
disablePBSOffline := m.config.DisableAllPBSOffline
|
||||
_, disablePBSOffline := m.alertPolicyTypeSwitchesNoLock("pbs")
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Check override disable BEFORE offline detection to prevent spurious notifications
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ func (m *Manager) CheckPMG(pmg models.PMGInstance) {
|
|||
m.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
if m.config.DisableAllPMG {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("pmg"); allDisabled {
|
||||
m.mu.RUnlock()
|
||||
// Clear any existing PMG alerts when all PMG alerts are disabled.
|
||||
m.mu.Lock()
|
||||
|
|
@ -86,7 +86,7 @@ func (m *Manager) CheckPMG(pmg models.PMGInstance) {
|
|||
|
||||
// Check if there's an override for this PMG instance
|
||||
override, hasOverride := m.config.Overrides[pmg.ID]
|
||||
disablePMGOffline := m.config.DisableAllPMGOffline
|
||||
_, disablePMGOffline := m.alertPolicyTypeSwitchesNoLock("pmg")
|
||||
pmgDefaults := m.config.PMGDefaults
|
||||
m.mu.RUnlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func (m *Manager) CheckStorage(storage models.Storage) {
|
|||
return
|
||||
}
|
||||
resourceIDs := storageAlertResourceIDs(storage)
|
||||
if m.config.DisableAllStorage {
|
||||
if allDisabled, _ := m.alertPolicyTypeSwitchesNoLock("storage"); allDisabled {
|
||||
m.mu.RUnlock()
|
||||
// Clear any existing storage alerts when all storage alerts are disabled
|
||||
m.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -249,13 +249,7 @@ func (m *Manager) resolveGuestThresholdOverride(base ThresholdConfig, guest any,
|
|||
// resolveResourceThresholds builds the effective thresholds for a resource type and ID.
|
||||
// Callers must hold m.mu when reading config through this helper.
|
||||
func (m *Manager) resolveResourceThresholds(typeKey, resourceID string) ThresholdConfig {
|
||||
if isGuestThresholdResourceType(typeKey) {
|
||||
return m.resolveGuestThresholdOverride(m.defaultThresholdsForResourceType(typeKey), nil, resourceID)
|
||||
}
|
||||
if typeKey == "storage" {
|
||||
return m.resolveStorageThresholdOverride(m.defaultThresholdsForResourceType(typeKey), resourceID, nil)
|
||||
}
|
||||
return m.resolveThresholdOverride(m.defaultThresholdsForResourceType(typeKey), resourceID)
|
||||
return m.effectiveAlertPolicyNoLock(alertPolicyQuery{TypeKey: typeKey, ResourceID: resourceID}).Thresholds
|
||||
}
|
||||
|
||||
// evaluateUnifiedMetrics runs the common metric dispatch path for unified resources.
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ func (m *Manager) SyncUnifiedResourceIncidents(resources []unifiedresources.Reso
|
|||
|
||||
m.mu.RLock()
|
||||
enabled := m.config.Enabled
|
||||
disableAllStorage := m.config.DisableAllStorage
|
||||
disableAllKubernetes := m.config.DisableAllKubernetes
|
||||
disableAllTrueNAS := m.config.DisableAllTrueNAS
|
||||
disableAllVMware := m.config.DisableAllVMware
|
||||
disableAllStorage, _ := m.alertPolicyTypeSwitchesNoLock("storage")
|
||||
disableAllKubernetes, _ := m.alertPolicyTypeSwitchesNoLock("k8s-node")
|
||||
disableAllTrueNAS, _ := m.alertPolicyTypeSwitchesNoLock("truenas-system")
|
||||
disableAllVMware, _ := m.alertPolicyTypeSwitchesNoLock("vmware-host")
|
||||
overrides := m.config.Overrides
|
||||
m.mu.RUnlock()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue