loop: let config turn a nudge rule off
The kuma service_down nudge cannot name the service. mavpoll folds the whole monitor_status gauge into one boolean fact keyed `service_down`, and the phraser names a service only when the fact key is the service name, so the message is always the generic "a service on homesrv is down". Every fifteen minutes, with nothing to act on. A fact per monitor is the real fix and it is filed as Vikunja #444; this is what to do until then. `disabled_rules` in mavend.json subtracts from loop.DefaultRules by name. Config only subtracts — rules stay code, the set stays canonical and ordered as written. A disabled rule is not gathered for either, since the gatherer derives its key set from the rules it was given. Unknown names are ignored so deleting a rule cannot brick a config that still lists it, and the boot log says what was dropped, because a rule that vanishes silently looks exactly like a rule that is broken. Deploy turns service_down off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
+35
-1
@@ -1,6 +1,9 @@
|
||||
package loop
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Rule — a proactive rule. Rules are CODE, not a DSL config — until ~30 rules
|
||||
// and you feel the pain (per spec). A Rule has a name (ids it in nudges.outcome
|
||||
@@ -137,6 +140,37 @@ func NetdataCriticalRule() Rule {
|
||||
}
|
||||
}
|
||||
|
||||
// RulesExcept returns DefaultRules minus the named ones (config's
|
||||
// `disabled_rules`). Config subtracts from the canonical set; it never adds to
|
||||
// it and never reorders it, so the "rules are code" line above still holds.
|
||||
//
|
||||
// Names are matched exactly and an unknown one is ignored, on purpose: a
|
||||
// config that still lists a rule someone deleted must not stop the daemon from
|
||||
// booting. The logging of what was actually dropped belongs to the caller,
|
||||
// which knows whether anyone is listening.
|
||||
func RulesExcept(disabled []string) ([]Rule, []string) {
|
||||
all := DefaultRules()
|
||||
if len(disabled) == 0 {
|
||||
return all, nil
|
||||
}
|
||||
off := make(map[string]bool, len(disabled))
|
||||
for _, n := range disabled {
|
||||
if n = strings.TrimSpace(n); n != "" {
|
||||
off[n] = true
|
||||
}
|
||||
}
|
||||
out := make([]Rule, 0, len(all))
|
||||
var dropped []string
|
||||
for _, r := range all {
|
||||
if off[r.Name] {
|
||||
dropped = append(dropped, r.Name)
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, dropped
|
||||
}
|
||||
|
||||
// DefaultRules — the canonical set the daemon wires. Add more as code, not config.
|
||||
// Order here is NOT load-bearing — the loop picks max severity, ties broken by
|
||||
// (severity desc, name asc) for deterministic output.
|
||||
|
||||
@@ -392,3 +392,45 @@ func TestPredicatesArePure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ruleNames(rs []Rule) []string {
|
||||
out := make([]string, len(rs))
|
||||
for i, r := range rs {
|
||||
out[i] = r.Name
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestRulesExceptDropsOnlyTheNamed(t *testing.T) {
|
||||
rules, dropped := RulesExcept([]string{"service_down"})
|
||||
if len(rules) != len(DefaultRules())-1 {
|
||||
t.Fatalf("got %v", ruleNames(rules))
|
||||
}
|
||||
for _, r := range rules {
|
||||
if r.Name == "service_down" {
|
||||
t.Error("a disabled rule was still wired")
|
||||
}
|
||||
}
|
||||
if len(dropped) != 1 || dropped[0] != "service_down" {
|
||||
t.Errorf("dropped = %v", dropped)
|
||||
}
|
||||
}
|
||||
|
||||
// A config that names a rule nobody wrote must not stop the daemon booting,
|
||||
// and must not quietly drop a real rule alongside it.
|
||||
func TestRulesExceptIgnoresUnknownNames(t *testing.T) {
|
||||
rules, dropped := RulesExcept([]string{"no_such_rule", " ", ""})
|
||||
if len(rules) != len(DefaultRules()) {
|
||||
t.Errorf("an unknown name removed something: %v", ruleNames(rules))
|
||||
}
|
||||
if len(dropped) != 0 {
|
||||
t.Errorf("dropped = %v, want nothing", dropped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRulesExceptEmptyKeepsEverything(t *testing.T) {
|
||||
rules, dropped := RulesExcept(nil)
|
||||
if len(rules) != len(DefaultRules()) || dropped != nil {
|
||||
t.Errorf("rules = %v, dropped = %v", ruleNames(rules), dropped)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user