Alerting: Calculate diff for two AlertRules (#45877)

* add custom diff reporter DiffReporter that reports only paths that have a difference
* create Diff method for AlertRule that returns DiffReport, which is an alias for []Diff

Tests:
* create copy method for AlertRule in testing
* create GenerateAlertQuery method in testing
This commit is contained in:
Yuriy Tseretyan
2022-02-28 17:13:53 +01:00
committed by GitHub
parent eb537e2efd
commit 4e19d7df63
4 changed files with 521 additions and 18 deletions
@@ -1,12 +1,14 @@
package models
import (
"encoding/json"
"math/rand"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/util"
@@ -225,3 +227,316 @@ func TestPatchPartialAlertRule(t *testing.T) {
}
})
}
func TestDiff(t *testing.T) {
t.Run("should return nil if there is no diff", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
result := rule1.Diff(rule2)
require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result)
})
t.Run("should respect fields to ignore", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
rule2.ID = rule1.ID/2 + 1
rule2.Version = rule1.Version/2 + 1
rule2.Updated = rule1.Updated.Add(1 * time.Second)
result := rule1.Diff(rule2, "ID", "Version", "Updated")
require.Emptyf(t, result, "expected diff to be empty. rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, result)
})
t.Run("should find diff in simple fields", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := AlertRuleGen()()
diffs := rule1.Diff(rule2, "Data", "Annotations", "Labels") // these fields will be tested separately
difCnt := 0
if rule1.ID != rule2.ID {
diff := diffs.GetDiffsForField("ID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.ID, diff[0].Left.Int())
assert.Equal(t, rule2.ID, diff[0].Right.Int())
difCnt++
}
if rule1.OrgID != rule2.OrgID {
diff := diffs.GetDiffsForField("OrgID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.OrgID, diff[0].Left.Int())
assert.Equal(t, rule2.OrgID, diff[0].Right.Int())
difCnt++
}
if rule1.Title != rule2.Title {
diff := diffs.GetDiffsForField("Title")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Title, diff[0].Left.String())
assert.Equal(t, rule2.Title, diff[0].Right.String())
difCnt++
}
if rule1.Condition != rule2.Condition {
diff := diffs.GetDiffsForField("Condition")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Condition, diff[0].Left.String())
assert.Equal(t, rule2.Condition, diff[0].Right.String())
difCnt++
}
if rule1.Updated != rule2.Updated {
diff := diffs.GetDiffsForField("Updated")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Updated, diff[0].Left.Interface())
assert.Equal(t, rule2.Updated, diff[0].Right.Interface())
difCnt++
}
if rule1.IntervalSeconds != rule2.IntervalSeconds {
diff := diffs.GetDiffsForField("IntervalSeconds")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.IntervalSeconds, diff[0].Left.Int())
assert.Equal(t, rule2.IntervalSeconds, diff[0].Right.Int())
difCnt++
}
if rule1.Version != rule2.Version {
diff := diffs.GetDiffsForField("Version")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.Version, diff[0].Left.Int())
assert.Equal(t, rule2.Version, diff[0].Right.Int())
difCnt++
}
if rule1.UID != rule2.UID {
diff := diffs.GetDiffsForField("UID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.UID, diff[0].Left.String())
assert.Equal(t, rule2.UID, diff[0].Right.String())
difCnt++
}
if rule1.NamespaceUID != rule2.NamespaceUID {
diff := diffs.GetDiffsForField("NamespaceUID")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.NamespaceUID, diff[0].Left.String())
assert.Equal(t, rule2.NamespaceUID, diff[0].Right.String())
difCnt++
}
if rule1.DashboardUID != rule2.DashboardUID {
diff := diffs.GetDiffsForField("DashboardUID")
assert.Len(t, diff, 1)
if rule1.DashboardUID == nil {
assert.True(t, diff[0].Left.IsNil())
} else {
assert.Equal(t, *rule1.DashboardUID, diff[0].Left.Elem().String())
}
if rule2.DashboardUID == nil {
assert.True(t, diff[0].Right.IsNil())
} else {
assert.Equal(t, *rule2.DashboardUID, diff[0].Right.Elem().String())
}
difCnt++
}
if rule1.PanelID != rule2.PanelID {
diff := diffs.GetDiffsForField("PanelID")
assert.Len(t, diff, 1)
if rule1.PanelID == nil {
assert.True(t, diff[0].Left.IsNil())
} else {
assert.Equal(t, *rule1.PanelID, diff[0].Left.Elem().Int())
}
if rule2.PanelID == nil {
assert.True(t, diff[0].Right.IsNil())
} else {
assert.Equal(t, *rule2.PanelID, diff[0].Right.Elem().Int())
}
difCnt++
}
if rule1.RuleGroup != rule2.RuleGroup {
diff := diffs.GetDiffsForField("RuleGroup")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.RuleGroup, diff[0].Left.String())
assert.Equal(t, rule2.RuleGroup, diff[0].Right.String())
difCnt++
}
if rule1.NoDataState != rule2.NoDataState {
diff := diffs.GetDiffsForField("NoDataState")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.NoDataState, diff[0].Left.Interface())
assert.Equal(t, rule2.NoDataState, diff[0].Right.Interface())
difCnt++
}
if rule1.ExecErrState != rule2.ExecErrState {
diff := diffs.GetDiffsForField("ExecErrState")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.ExecErrState, diff[0].Left.Interface())
assert.Equal(t, rule2.ExecErrState, diff[0].Right.Interface())
difCnt++
}
if rule1.For != rule2.For {
diff := diffs.GetDiffsForField("For")
assert.Len(t, diff, 1)
assert.Equal(t, rule1.For, diff[0].Left.Interface())
assert.Equal(t, rule2.For, diff[0].Right.Interface())
difCnt++
}
require.Lenf(t, diffs, difCnt, "Got some unexpected diffs. Either add to ignore or add assert to it")
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, diffs)
}
})
t.Run("should detect changes in Annotations", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
rule1.Annotations = map[string]string{
"key1": "value1",
"key2": "value2",
}
rule2.Annotations = map[string]string{
"key2": "value22",
"key3": "value3",
}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 3)
d := diff.GetDiffsForField("Annotations[key1]")
assert.Len(t, d, 1)
assert.Equal(t, "value1", d[0].Left.String())
assert.False(t, d[0].Right.IsValid())
d = diff.GetDiffsForField("Annotations[key2]")
assert.Len(t, d, 1)
assert.Equal(t, "value2", d[0].Left.String())
assert.Equal(t, "value22", d[0].Right.String())
d = diff.GetDiffsForField("Annotations[key3]")
assert.Len(t, d, 1)
assert.False(t, d[0].Left.IsValid())
assert.Equal(t, "value3", d[0].Right.String())
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
t.Run("should detect changes in Labels", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
rule1.Labels = map[string]string{
"key1": "value1",
"key2": "value2",
}
rule2.Labels = map[string]string{
"key2": "value22",
"key3": "value3",
}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 3)
d := diff.GetDiffsForField("Labels[key1]")
assert.Len(t, d, 1)
assert.Equal(t, "value1", d[0].Left.String())
assert.False(t, d[0].Right.IsValid())
d = diff.GetDiffsForField("Labels[key2]")
assert.Len(t, d, 1)
assert.Equal(t, "value2", d[0].Left.String())
assert.Equal(t, "value22", d[0].Right.String())
d = diff.GetDiffsForField("Labels[key3]")
assert.Len(t, d, 1)
assert.False(t, d[0].Left.IsValid())
assert.Equal(t, "value3", d[0].Right.String())
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %s", rule1, rule2, d)
}
})
t.Run("should detect changes in Data", func(t *testing.T) {
rule1 := AlertRuleGen()()
rule2 := CopyRule(rule1)
query1 := AlertQuery{
RefID: "A",
QueryType: util.GenerateShortUID(),
RelativeTimeRange: RelativeTimeRange{
From: Duration(5 * time.Hour),
To: 0,
},
DatasourceUID: util.GenerateShortUID(),
Model: json.RawMessage(`{ "test": "data"}`),
modelProps: map[string]interface{}{
"test": 1,
},
}
rule1.Data = []AlertQuery{query1}
t.Run("should ignore modelProps", func(t *testing.T) {
query2 := query1
query2.modelProps = map[string]interface{}{
"some": "other value",
}
rule2.Data = []AlertQuery{query2}
diff := rule1.Diff(rule2)
assert.Nil(t, diff)
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
t.Run("should detect changes inside the query", func(t *testing.T) {
query2 := query1
query2.QueryType = "test"
query2.RefID = "test"
rule2.Data = []AlertQuery{query2}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 2)
d := diff.GetDiffsForField("Data[0].QueryType")
assert.Len(t, d, 1)
d = diff.GetDiffsForField("Data[0].RefID")
assert.Len(t, d, 1)
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
t.Run("should detect new changes in array if too many fields changed", func(t *testing.T) {
query2 := query1
query2.QueryType = "test"
query2.RefID = "test"
query2.DatasourceUID = "test"
query2.Model = json.RawMessage(`{ "test": "da2ta"}`)
rule2.Data = []AlertQuery{query2}
diff := rule1.Diff(rule2)
assert.Len(t, diff, 2)
for _, d := range diff {
assert.Equal(t, "Data", d.Path)
if d.Left.IsValid() {
assert.Equal(t, query1, d.Left.Interface())
} else {
assert.Equal(t, query2, d.Right.Interface())
}
}
if t.Failed() {
t.Logf("rule1: %#v, rule2: %#v\ndiff: %v", rule1, rule2, diff)
}
})
})
}