Files
Felis/internal/metrics/alerts_test.go

99 lines
2.8 KiB
Go

package metrics
import (
"os"
"reflect"
"regexp"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/yaml"
)
// The shipped alert rules live in deploy/alerts. promtool tests what they do
// (felis-alerts_test.yml); these tests pin what promtool cannot see from there.
type ruleGroups struct {
Groups []struct {
Name string `json:"name"`
Rules []struct {
Alert string `json:"alert"`
Expr string `json:"expr"`
} `json:"rules"`
} `json:"groups"`
}
func readYAML(t *testing.T, path string, into any) {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if err := yaml.Unmarshal(raw, into); err != nil {
t.Fatalf("parse %s: %v", path, err)
}
}
// TestPrometheusRuleMatchesPlainRules: the PrometheusRule twin carries exactly
// the groups of the promtool-tested plain file.
func TestPrometheusRuleMatchesPlainRules(t *testing.T) {
var plain, twin map[string]any
readYAML(t, "../../deploy/alerts/felis-alerts.yaml", &plain)
readYAML(t, "../../deploy/alerts/felis-prometheusrule.yaml", &twin)
spec, _ := twin["spec"].(map[string]any)
if !reflect.DeepEqual(plain["groups"], spec["groups"]) {
t.Fatal("deploy/alerts/felis-prometheusrule.yaml spec.groups differs from felis-alerts.yaml groups; copy the plain file's groups over")
}
}
// TestAlertRulesUseRealMetrics: every felis_* series a rule reads is one this
// package exports (or the backup timer's textfile series), so a renamed metric
// cannot leave an alert silently watching nothing.
func TestAlertRulesUseRealMetrics(t *testing.T) {
reg := prometheus.NewRegistry()
if err := Register(reg); err != nil {
t.Fatal(err)
}
families, err := reg.Gather()
if err != nil {
t.Fatal(err)
}
known := map[string]bool{}
for _, mf := range families {
known[mf.GetName()] = true
}
// Collectors with no children yet gather nothing; name them from their
// descriptors instead.
for _, c := range Collectors() {
ch := make(chan *prometheus.Desc, 4)
c.Describe(ch)
close(ch)
for d := range ch {
if m := regexp.MustCompile(`fqName: "([^"]+)"`).FindStringSubmatch(d.String()); m != nil {
known[m[1]] = true
}
}
}
var rules ruleGroups
readYAML(t, "../../deploy/alerts/felis-alerts.yaml", &rules)
series := regexp.MustCompile(`felis_[a-z_]+`)
for _, g := range rules.Groups {
for _, r := range g.Rules {
for _, name := range series.FindAllString(r.Expr, -1) {
if strings.HasPrefix(name, "felis_db_backup_") {
continue // written by felis-db-backup.timer for node-exporter
}
base := name
for _, suffix := range []string{"_bucket", "_count", "_sum"} {
base = strings.TrimSuffix(base, suffix)
}
if !known[name] && !known[base] {
t.Errorf("alert %s reads %s, which no felis collector exports", r.Alert, name)
}
}
}
}
}