From c843a0cd29e8b4c42b5201659d25dc2a76185410 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Wed, 7 Jun 2017 16:58:15 +0200 Subject: [PATCH] pkg/rulefmt: Add rule group parsing --- pkg/rulefmt/rulefmt.go | 98 ++++++++++++++++++++++++++++++++++ pkg/rulefmt/rulefmt_test.go | 12 +++++ pkg/rulefmt/testdata/test.yaml | 59 ++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 pkg/rulefmt/rulefmt.go create mode 100644 pkg/rulefmt/rulefmt_test.go create mode 100644 pkg/rulefmt/testdata/test.yaml diff --git a/pkg/rulefmt/rulefmt.go b/pkg/rulefmt/rulefmt.go new file mode 100644 index 000000000..0d1796d9e --- /dev/null +++ b/pkg/rulefmt/rulefmt.go @@ -0,0 +1,98 @@ +package rulefmt + +import ( + "io/ioutil" + + "github.com/ghodss/yaml" + "github.com/pkg/errors" + "github.com/prometheus/prometheus/promql" +) + +// Error represents semantical errors on parsing rule groups. +type Error struct { + Group string + Rule int + Err error +} + +func (err *Error) Error() string { + return errors.Wrapf(err, "group %q, rule %d", err.Group, err.Rule).Error() +} + +// RuleGroups is a set of rule groups that are typically exposed in a file. +type RuleGroups struct { + Version int `json:"version"` + Groups []RuleGroup `json:"groups"` +} + +// Validate validates all rules in the rule groups. +func (g *RuleGroups) Validate() (errs []error) { + if g.Version != 1 { + errs = append(errs, errors.Errorf("invalid rule group version %d", g.Version)) + } + for _, g := range g.Groups { + for i, r := range g.Rules { + for _, err := range r.Validate() { + errs = append(errs, &Error{ + Group: g.Name, + Rule: i, + Err: err, + }) + } + } + } + return errs +} + +// RuleGroup is a list of sequentially evaluated recording and alerting rules. +type RuleGroup struct { + Name string `json:"name"` + Rules []Rule `json:"rules"` +} + +// Rule describes an alerting or recording rule. +type Rule struct { + Record string `json:"record"` + Alert string `json:"alert"` + Expr string `json:"expr"` + For string `json:"for"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` +} + +// Validate the rule and return a list of encountered errors. +func (r *Rule) Validate() (errs []error) { + if r.Record != "" && r.Alert != "" { + errs = append(errs, errors.Errorf("only one of 'record' and 'alert' must be set")) + } + if r.Record == "" && r.Alert == "" { + errs = append(errs, errors.Errorf("one of 'record' or 'alert' must be set")) + } + if r.Expr == "" { + errs = append(errs, errors.Errorf("field 'expr' must be set in rule")) + } else if _, err := promql.ParseExpr(r.Expr); err != nil { + errs = append(errs, errors.Errorf("could not parse expression: %s", err)) + } + if r.Record != "" { + if len(r.Annotations) > 0 { + errs = append(errs, errors.Errorf("invalid field 'annotations' in recording rule")) + } + if r.For != "" { + errs = append(errs, errors.Errorf("invalid field 'for' in recording rule")) + } + } + return errs +} + +// ParseFile parses the rule file and validates it. +func ParseFile(file string) (*RuleGroups, []error) { + b, err := ioutil.ReadFile(file) + if err != nil { + return nil, []error{err} + } + var groups RuleGroups + if err := yaml.Unmarshal(b, &groups); err != nil { + return nil, []error{err} + } + return &groups, groups.Validate() +} diff --git a/pkg/rulefmt/rulefmt_test.go b/pkg/rulefmt/rulefmt_test.go new file mode 100644 index 000000000..494a8ee50 --- /dev/null +++ b/pkg/rulefmt/rulefmt_test.go @@ -0,0 +1,12 @@ +package rulefmt + +import "testing" + +func TestParseFileSuccess(t *testing.T) { + if _, errs := ParseFile("testdata/test.yaml"); len(errs) > 0 { + t.Errorf("unexpected errors parsing file") + for _, err := range errs { + t.Error(err) + } + } +} diff --git a/pkg/rulefmt/testdata/test.yaml b/pkg/rulefmt/testdata/test.yaml new file mode 100644 index 000000000..321b0268f --- /dev/null +++ b/pkg/rulefmt/testdata/test.yaml @@ -0,0 +1,59 @@ +version: 1 +groups: +- name: my-group-name + interval: 30s # defaults to global interval + rules: + - alert: HighErrors + expr: | + sum without(instance) (rate(errors_total[5m])) + / sum without(instance) (rate(requests_total[5m])) + for: 5m + labels: + severity: critical + annotations: + description: "stuff's happening with {{ $.labels.service }}" + + # Mix recording rules in the same list + - record: "new_metric" + expr: | + sum without(instance) (rate(errors_total[5m])) + / sum without(instance) (rate(requests_total[5m])) + labels: + abc: edf + uvw: xyz + + - alert: HighErrors + expr: | + sum without(instance) (rate(errors_total[5m])) + / sum without(instance) (rate(requests_total[5m])) + for: 5m + labels: + severity: critical + annotations: + description: "stuff's happening with {{ $.labels.service }}" + +- name: my-group-name + interval: 30s # defaults to global interval + rules: + - alert: HighErrors + expr: | + sum without(instance) (rate(errors_total[5m])) + / sum without(instance) (rate(requests_total[5m])) + for: 5m + labels: + severity: critical + + - record: "new_metric" + expr: | + sum without(instance) (rate(errors_total[5m])) + / sum without(instance) (rate(requests_total[5m])) + + - alert: HighErrors + expr: | + sum without(instance) (rate(errors_total[5m])) + / sum without(instance) (rate(requests_total[5m])) + for: 5m + labels: + severity: critical + annotations: + description: "stuff's happening with {{ $.labels.service }}"