mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-02-19 15:30:17 +00:00
vmalert-537: allow name duplication for rules within one group. (#559)
Uniqueness of rule is now defined by combination of its name, expression and labels. The hash of the combination is now used as rule ID and identifies rule within the group. Set of rules from coreos/kube-prometheus was added for testing purposes to verify compatibility. The check also showed that `vmalert` doesn't support `query` template function that was mentioned as limitation in README.
This commit is contained in:
parent
87151e825e
commit
1a01fe2cf2
8 changed files with 2012 additions and 57 deletions
|
@ -20,6 +20,7 @@ may fail;
|
||||||
* by default, rules execution is sequential within one group, but persisting of execution results to remote
|
* by default, rules execution is sequential within one group, but persisting of execution results to remote
|
||||||
storage is asynchronous. Hence, user shouldn't rely on recording rules chaining when result of previous
|
storage is asynchronous. Hence, user shouldn't rely on recording rules chaining when result of previous
|
||||||
recording rule is reused in next one;
|
recording rule is reused in next one;
|
||||||
|
* there is no `query` function support in templates yet;
|
||||||
* `vmalert` has no UI, just an API for getting groups and rules statuses.
|
* `vmalert` has no UI, just an API for getting groups and rules statuses.
|
||||||
|
|
||||||
### QuickStart
|
### QuickStart
|
||||||
|
@ -85,6 +86,9 @@ and to send notifications about firing alerts to [Alertmanager](https://github.c
|
||||||
Recording rules allow you to precompute frequently needed or computationally expensive expressions
|
Recording rules allow you to precompute frequently needed or computationally expensive expressions
|
||||||
and save their result as a new set of time series.
|
and save their result as a new set of time series.
|
||||||
|
|
||||||
|
`vmalert` forbids to define duplicates - rules with the same combination of name, expression and labels
|
||||||
|
within one group.
|
||||||
|
|
||||||
##### Alerting rules
|
##### Alerting rules
|
||||||
|
|
||||||
The syntax for alerting rule is following:
|
The syntax for alerting rule is following:
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
|
|
||||||
// AlertingRule is basic alert entity
|
// AlertingRule is basic alert entity
|
||||||
type AlertingRule struct {
|
type AlertingRule struct {
|
||||||
|
RuleID uint64
|
||||||
Name string
|
Name string
|
||||||
Expr string
|
Expr string
|
||||||
For time.Duration
|
For time.Duration
|
||||||
|
@ -39,6 +40,7 @@ type AlertingRule struct {
|
||||||
|
|
||||||
func newAlertingRule(gID uint64, cfg config.Rule) *AlertingRule {
|
func newAlertingRule(gID uint64, cfg config.Rule) *AlertingRule {
|
||||||
return &AlertingRule{
|
return &AlertingRule{
|
||||||
|
RuleID: cfg.ID,
|
||||||
Name: cfg.Alert,
|
Name: cfg.Alert,
|
||||||
Expr: cfg.Expr,
|
Expr: cfg.Expr,
|
||||||
For: cfg.For,
|
For: cfg.For,
|
||||||
|
@ -57,11 +59,7 @@ func (ar *AlertingRule) String() string {
|
||||||
// ID returns unique Rule ID
|
// ID returns unique Rule ID
|
||||||
// within the parent Group.
|
// within the parent Group.
|
||||||
func (ar *AlertingRule) ID() uint64 {
|
func (ar *AlertingRule) ID() uint64 {
|
||||||
hash := fnv.New64a()
|
return ar.RuleID
|
||||||
hash.Write([]byte("alerting"))
|
|
||||||
hash.Write([]byte("\xff"))
|
|
||||||
hash.Write([]byte(ar.Name))
|
|
||||||
return hash.Sum64()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec executes AlertingRule expression via the given Querier.
|
// Exec executes AlertingRule expression via the given Querier.
|
||||||
|
|
|
@ -2,14 +2,16 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v2"
|
"hash/fnv"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Group contains list of Rules grouped into
|
// Group contains list of Rules grouped into
|
||||||
|
@ -33,16 +35,16 @@ func (g *Group) Validate(validateAnnotations, validateExpressions bool) error {
|
||||||
if len(g.Rules) == 0 {
|
if len(g.Rules) == 0 {
|
||||||
return fmt.Errorf("group %q can't contain no rules", g.Name)
|
return fmt.Errorf("group %q can't contain no rules", g.Name)
|
||||||
}
|
}
|
||||||
uniqueRules := map[string]struct{}{}
|
uniqueRules := map[uint64]struct{}{}
|
||||||
for _, r := range g.Rules {
|
for _, r := range g.Rules {
|
||||||
ruleName := r.Record
|
ruleName := r.Record
|
||||||
if r.Alert != "" {
|
if r.Alert != "" {
|
||||||
ruleName = r.Alert
|
ruleName = r.Alert
|
||||||
}
|
}
|
||||||
if _, ok := uniqueRules[ruleName]; ok {
|
if _, ok := uniqueRules[r.ID]; ok {
|
||||||
return fmt.Errorf("rule name %q duplicate", ruleName)
|
return fmt.Errorf("rule %q duplicate", ruleName)
|
||||||
}
|
}
|
||||||
uniqueRules[ruleName] = struct{}{}
|
uniqueRules[r.ID] = struct{}{}
|
||||||
if err := r.Validate(); err != nil {
|
if err := r.Validate(); err != nil {
|
||||||
return fmt.Errorf("invalid rule %q.%q: %s", g.Name, ruleName, err)
|
return fmt.Errorf("invalid rule %q.%q: %s", g.Name, ruleName, err)
|
||||||
}
|
}
|
||||||
|
@ -66,12 +68,56 @@ func (g *Group) Validate(validateAnnotations, validateExpressions bool) error {
|
||||||
// Rule describes entity that represent either
|
// Rule describes entity that represent either
|
||||||
// recording rule or alerting rule.
|
// recording rule or alerting rule.
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
|
ID uint64
|
||||||
Record string `yaml:"record,omitempty"`
|
Record string `yaml:"record,omitempty"`
|
||||||
Alert string `yaml:"alert,omitempty"`
|
Alert string `yaml:"alert,omitempty"`
|
||||||
Expr string `yaml:"expr"`
|
Expr string `yaml:"expr"`
|
||||||
For time.Duration `yaml:"for,omitempty"`
|
For time.Duration `yaml:"for,omitempty"`
|
||||||
Labels map[string]string `yaml:"labels,omitempty"`
|
Labels map[string]string `yaml:"labels,omitempty"`
|
||||||
Annotations map[string]string `yaml:"annotations,omitempty"`
|
Annotations map[string]string `yaml:"annotations,omitempty"`
|
||||||
|
|
||||||
|
// Catches all undefined fields and must be empty after parsing.
|
||||||
|
XXX map[string]interface{} `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||||
|
func (r *Rule) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rule Rule
|
||||||
|
if err := unmarshal((*rule)(r)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.ID = HashRule(*r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashRule hashes significant Rule fields into
|
||||||
|
// unique hash value
|
||||||
|
func HashRule(r Rule) uint64 {
|
||||||
|
h := fnv.New64a()
|
||||||
|
h.Write([]byte(r.Expr))
|
||||||
|
if r.Record != "" {
|
||||||
|
h.Write([]byte("recording"))
|
||||||
|
h.Write([]byte(r.Record))
|
||||||
|
} else {
|
||||||
|
h.Write([]byte("alerting"))
|
||||||
|
h.Write([]byte(r.Alert))
|
||||||
|
}
|
||||||
|
type item struct {
|
||||||
|
key, value string
|
||||||
|
}
|
||||||
|
var kv []item
|
||||||
|
for k, v := range r.Labels {
|
||||||
|
kv = append(kv, item{key: k, value: v})
|
||||||
|
}
|
||||||
|
sort.Slice(kv, func(i, j int) bool {
|
||||||
|
return kv[i].key < kv[j].key
|
||||||
|
})
|
||||||
|
for _, i := range kv {
|
||||||
|
h.Write([]byte(i.key))
|
||||||
|
h.Write([]byte(i.value))
|
||||||
|
h.Write([]byte("\xff"))
|
||||||
|
}
|
||||||
|
return h.Sum64()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate check for Rule configuration errors
|
// Validate check for Rule configuration errors
|
||||||
|
@ -82,7 +128,7 @@ func (r *Rule) Validate() error {
|
||||||
if r.Expr == "" {
|
if r.Expr == "" {
|
||||||
return fmt.Errorf("expression can't be empty")
|
return fmt.Errorf("expression can't be empty")
|
||||||
}
|
}
|
||||||
return nil
|
return checkOverflow(r.XXX, "rule")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parses rule configs from given file patterns
|
// Parse parses rule configs from given file patterns
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||||
)
|
)
|
||||||
|
@ -147,6 +148,73 @@ func TestGroup_Validate(t *testing.T) {
|
||||||
expErr: "error parsing annotation",
|
expErr: "error parsing annotation",
|
||||||
validateAnnotations: true,
|
validateAnnotations: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: &Group{Name: "test",
|
||||||
|
Rules: []Rule{
|
||||||
|
{
|
||||||
|
Alert: "alert",
|
||||||
|
Expr: "up == 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Alert: "alert",
|
||||||
|
Expr: "up == 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: "duplicate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: &Group{Name: "test",
|
||||||
|
Rules: []Rule{
|
||||||
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"summary": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"summary": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: "duplicate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: &Group{Name: "test",
|
||||||
|
Rules: []Rule{
|
||||||
|
{Record: "record", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"summary": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
{Record: "record", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"summary": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: "duplicate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: &Group{Name: "test",
|
||||||
|
Rules: []Rule{
|
||||||
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"summary": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"description": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: &Group{Name: "test",
|
||||||
|
Rules: []Rule{
|
||||||
|
{Record: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"summary": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"summary": "{{ value|query }}",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
err := tc.group.Validate(tc.validateAnnotations, tc.validateExpressions)
|
err := tc.group.Validate(tc.validateAnnotations, tc.validateExpressions)
|
||||||
|
@ -161,3 +229,98 @@ func TestGroup_Validate(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHashRule(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
a, b Rule
|
||||||
|
equal bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Rule{Record: "record", Expr: "up == 1"},
|
||||||
|
Rule{Record: "record", Expr: "up == 1"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1"},
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
}},
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
}},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
}},
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"baz": "foo",
|
||||||
|
"foo": "bar",
|
||||||
|
}},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "record", Expr: "up == 1"},
|
||||||
|
Rule{Alert: "record", Expr: "up == 1"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", For: time.Minute},
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "record", Expr: "up == 1"},
|
||||||
|
Rule{Record: "record", Expr: "up == 1"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Record: "record", Expr: "up == 1"},
|
||||||
|
Rule{Record: "record", Expr: "up == 2"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
}},
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"baz": "foo",
|
||||||
|
"foo": "baz",
|
||||||
|
}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
}},
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"baz": "foo",
|
||||||
|
}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
}},
|
||||||
|
Rule{Alert: "alert", Expr: "up == 1"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tc := range testCases {
|
||||||
|
aID, bID := HashRule(tc.a), HashRule(tc.b)
|
||||||
|
if tc.equal != (aID == bID) {
|
||||||
|
t.Fatalf("missmatch for rule %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1727
app/vmalert/config/testdata/kube-good.rules
vendored
Normal file
1727
app/vmalert/config/testdata/kube-good.rules
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -19,6 +19,12 @@ groups:
|
||||||
- record: code:requests:rate5m
|
- record: code:requests:rate5m
|
||||||
expr: sum(rate(promhttp_metric_handler_requests_total[5m])) by (code)
|
expr: sum(rate(promhttp_metric_handler_requests_total[5m])) by (code)
|
||||||
labels:
|
labels:
|
||||||
|
env: dev
|
||||||
|
recording: true
|
||||||
|
- record: code:requests:rate5m
|
||||||
|
expr: sum(rate(promhttp_metric_handler_requests_total[5m])) by (code)
|
||||||
|
labels:
|
||||||
|
env: staging
|
||||||
recording: true
|
recording: true
|
||||||
- record: successful_requests:ratio_rate5m
|
- record: successful_requests:ratio_rate5m
|
||||||
labels:
|
labels:
|
||||||
|
|
|
@ -13,21 +13,20 @@ import (
|
||||||
func TestUpdateWith(t *testing.T) {
|
func TestUpdateWith(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
currentRules []Rule
|
currentRules []config.Rule
|
||||||
// rules must be sorted by ID
|
newRules []config.Rule
|
||||||
newRules []Rule
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"new rule",
|
"new rule",
|
||||||
[]Rule{},
|
nil,
|
||||||
[]Rule{&AlertingRule{Name: "bar"}},
|
[]config.Rule{{Alert: "bar"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"update alerting rule",
|
"update alerting rule",
|
||||||
[]Rule{&AlertingRule{
|
[]config.Rule{{
|
||||||
Name: "foo",
|
Alert: "foo",
|
||||||
Expr: "up > 0",
|
Expr: "up > 0",
|
||||||
For: time.Second,
|
For: time.Second,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"bar": "baz",
|
"bar": "baz",
|
||||||
},
|
},
|
||||||
|
@ -36,10 +35,10 @@ func TestUpdateWith(t *testing.T) {
|
||||||
"description": "{{$labels}}",
|
"description": "{{$labels}}",
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
[]Rule{&AlertingRule{
|
[]config.Rule{{
|
||||||
Name: "foo",
|
Alert: "foo",
|
||||||
Expr: "up > 10",
|
Expr: "up > 10",
|
||||||
For: time.Second,
|
For: time.Second,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"baz": "bar",
|
"baz": "bar",
|
||||||
},
|
},
|
||||||
|
@ -50,16 +49,16 @@ func TestUpdateWith(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"update recording rule",
|
"update recording rule",
|
||||||
[]Rule{&RecordingRule{
|
[]config.Rule{{
|
||||||
Name: "foo",
|
Record: "foo",
|
||||||
Expr: "max(up)",
|
Expr: "max(up)",
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"bar": "baz",
|
"bar": "baz",
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
[]Rule{&RecordingRule{
|
[]config.Rule{{
|
||||||
Name: "foo",
|
Record: "foo",
|
||||||
Expr: "min(up)",
|
Expr: "min(up)",
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"baz": "bar",
|
"baz": "bar",
|
||||||
},
|
},
|
||||||
|
@ -67,45 +66,56 @@ func TestUpdateWith(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty rule",
|
"empty rule",
|
||||||
[]Rule{&AlertingRule{Name: "foo"}, &RecordingRule{Name: "bar"}},
|
[]config.Rule{{Alert: "foo"}, {Record: "bar"}},
|
||||||
[]Rule{},
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"multiple rules",
|
"multiple rules",
|
||||||
[]Rule{
|
[]config.Rule{
|
||||||
&AlertingRule{Name: "bar"},
|
{Alert: "bar"},
|
||||||
&AlertingRule{Name: "baz"},
|
{Alert: "baz"},
|
||||||
&RecordingRule{Name: "foo"},
|
{Alert: "foo"},
|
||||||
},
|
},
|
||||||
[]Rule{
|
[]config.Rule{
|
||||||
&AlertingRule{Name: "baz"},
|
{Alert: "baz"},
|
||||||
&RecordingRule{Name: "foo"},
|
{Record: "foo"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"replace rule",
|
"replace rule",
|
||||||
[]Rule{&AlertingRule{Name: "foo1"}},
|
[]config.Rule{{Alert: "foo1"}},
|
||||||
[]Rule{&AlertingRule{Name: "foo2"}},
|
[]config.Rule{{Alert: "foo2"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"replace multiple rules",
|
"replace multiple rules",
|
||||||
[]Rule{
|
[]config.Rule{
|
||||||
&AlertingRule{Name: "foo1"},
|
{Alert: "foo1"},
|
||||||
&RecordingRule{Name: "foo2"},
|
{Record: "foo2"},
|
||||||
&AlertingRule{Name: "foo3"},
|
{Alert: "foo3"},
|
||||||
},
|
},
|
||||||
[]Rule{
|
[]config.Rule{
|
||||||
&AlertingRule{Name: "foo3"},
|
{Alert: "foo3"},
|
||||||
&AlertingRule{Name: "foo4"},
|
{Alert: "foo4"},
|
||||||
&RecordingRule{Name: "foo5"},
|
{Record: "foo5"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
g := &Group{Rules: tc.currentRules}
|
g := &Group{Name: "test"}
|
||||||
err := g.updateWith(&Group{Rules: tc.newRules})
|
for _, r := range tc.currentRules {
|
||||||
|
r.ID = config.HashRule(r)
|
||||||
|
g.Rules = append(g.Rules, g.newRule(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
ng := &Group{Name: "test"}
|
||||||
|
for _, r := range tc.newRules {
|
||||||
|
r.ID = config.HashRule(r)
|
||||||
|
ng.Rules = append(ng.Rules, ng.newRule(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := g.updateWith(ng)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -117,8 +127,11 @@ func TestUpdateWith(t *testing.T) {
|
||||||
sort.Slice(g.Rules, func(i, j int) bool {
|
sort.Slice(g.Rules, func(i, j int) bool {
|
||||||
return g.Rules[i].ID() < g.Rules[j].ID()
|
return g.Rules[i].ID() < g.Rules[j].ID()
|
||||||
})
|
})
|
||||||
|
sort.Slice(ng.Rules, func(i, j int) bool {
|
||||||
|
return ng.Rules[i].ID() < ng.Rules[j].ID()
|
||||||
|
})
|
||||||
for i, r := range g.Rules {
|
for i, r := range g.Rules {
|
||||||
got, want := r, tc.newRules[i]
|
got, want := r, ng.Rules[i]
|
||||||
if got.ID() != want.ID() {
|
if got.ID() != want.ID() {
|
||||||
t.Fatalf("expected to have rule %q; got %q", want, got)
|
t.Fatalf("expected to have rule %q; got %q", want, got)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
// to evaluate configured Expression and
|
// to evaluate configured Expression and
|
||||||
// return TimeSeries as result.
|
// return TimeSeries as result.
|
||||||
type RecordingRule struct {
|
type RecordingRule struct {
|
||||||
|
RuleID uint64
|
||||||
Name string
|
Name string
|
||||||
Expr string
|
Expr string
|
||||||
Labels map[string]string
|
Labels map[string]string
|
||||||
|
@ -41,15 +42,12 @@ func (rr *RecordingRule) String() string {
|
||||||
// ID returns unique Rule ID
|
// ID returns unique Rule ID
|
||||||
// within the parent Group.
|
// within the parent Group.
|
||||||
func (rr *RecordingRule) ID() uint64 {
|
func (rr *RecordingRule) ID() uint64 {
|
||||||
hash := fnv.New64a()
|
return rr.RuleID
|
||||||
hash.Write([]byte("alerting"))
|
|
||||||
hash.Write([]byte("\xff"))
|
|
||||||
hash.Write([]byte(rr.Name))
|
|
||||||
return hash.Sum64()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRecordingRule(gID uint64, cfg config.Rule) *RecordingRule {
|
func newRecordingRule(gID uint64, cfg config.Rule) *RecordingRule {
|
||||||
return &RecordingRule{
|
return &RecordingRule{
|
||||||
|
RuleID: cfg.ID,
|
||||||
Name: cfg.Record,
|
Name: cfg.Record,
|
||||||
Expr: cfg.Expr,
|
Expr: cfg.Expr,
|
||||||
Labels: cfg.Labels,
|
Labels: cfg.Labels,
|
||||||
|
|
Loading…
Reference in a new issue