Alerting: Rule Version API to Ignore versions without diff (#100093)

This commit is contained in:
Yuri Tseretyan 2025-02-10 09:20:35 -05:00 committed by GitHub
parent dec07c4c34
commit 1b8db233a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 114 additions and 1 deletions

View File

@ -563,6 +563,12 @@ func (a *AlertRuleMutators) WithKey(key AlertRuleKey) AlertRuleMutator {
} }
} }
func (a *AlertRuleMutators) WithVersion(version int64) AlertRuleMutator {
return func(r *AlertRule) {
r.Version = version
}
}
func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels { func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels {
count := max count := max
if min > max { if min > max {

View File

@ -121,11 +121,12 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler
func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRuleKey) ([]*ngmodels.AlertRule, error) { func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRuleKey) ([]*ngmodels.AlertRule, error) {
alertRules := make([]*ngmodels.AlertRule, 0) alertRules := make([]*ngmodels.AlertRule, 0)
err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID).Desc("id").Rows(new(alertRuleVersion)) rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID).Asc("id").Rows(new(alertRuleVersion))
if err != nil { if err != nil {
return err return err
} }
// Deserialize each rule separately in case any of them contain invalid JSON. // Deserialize each rule separately in case any of them contain invalid JSON.
var previousVersion *alertRuleVersion
for rows.Next() { for rows.Next() {
rule := new(alertRuleVersion) rule := new(alertRuleVersion)
err = rows.Scan(rule) err = rows.Scan(rule)
@ -133,11 +134,17 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRu
st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "GetAlertRuleVersions", "error", err) st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "GetAlertRuleVersions", "error", err)
continue continue
} }
// skip version that has no diff with previous version
// this is pretty basic comparison, it may have false negatives
if previousVersion != nil && previousVersion.EqualSpec(*rule) {
continue
}
converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger) converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger)
if err != nil { if err != nil {
st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID) st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID)
continue continue
} }
previousVersion = rule
alertRules = append(alertRules, &converted) alertRules = append(alertRules, &converted)
} }
return nil return nil
@ -145,6 +152,15 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRu
if err != nil { if err != nil {
return nil, err return nil, err
} }
slices.SortFunc(alertRules, func(a, b *ngmodels.AlertRule) int {
if a.ID > b.ID {
return -1
}
if a.ID < b.ID {
return 1
}
return 0
})
return alertRules, nil return alertRules, nil
} }

View File

@ -1543,6 +1543,74 @@ func TestIncreaseVersionForAllRulesInNamespaces(t *testing.T) {
}) })
} }
func TestGetRuleVersions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
cfg := setting.NewCfg()
cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second}
sqlStore := db.InitTestDB(t)
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
b := &fakeBus{}
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
orgID := int64(1)
gen := models.RuleGen
gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithOrgID(orgID), gen.WithVersion(1))
inserted, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, []models.AlertRule{gen.Generate()})
require.NoError(t, err)
ruleV1, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: inserted[0].UID})
require.NoError(t, err)
ruleV2 := models.CopyRule(ruleV1, gen.WithTitle(util.GenerateShortUID()), gen.WithGroupIndex(rand.Int()))
err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{
{
Existing: ruleV1,
New: *ruleV2,
},
})
require.NoError(t, err)
t.Run("should return rule versions sorted in decreasing order", func(t *testing.T) {
versions, err := store.GetAlertRuleVersions(context.Background(), ruleV2.GetKey())
require.NoError(t, err)
assert.Len(t, versions, 2)
assert.IsDecreasing(t, versions[0].ID, versions[1].ID)
diff := versions[1].Diff(versions[0], AlertRuleFieldsToIgnoreInDiff[:]...)
assert.ElementsMatch(t, []string{"Title", "RuleGroupIndex"}, diff.Paths())
})
t.Run("should not remove versions without diff", func(t *testing.T) {
for i := 0; i < rand.Intn(2)+1; i++ {
r, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: ruleV2.UID})
require.NoError(t, err)
rn := models.CopyRule(r)
err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{
{
Existing: r,
New: *rn,
},
})
require.NoError(t, err)
}
ruleV2, err = store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: ruleV2.UID})
ruleV3 := models.CopyRule(ruleV2, gen.WithGroupName(util.GenerateShortUID()), gen.WithNamespaceUID(util.GenerateShortUID()))
err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{
{
Existing: ruleV2,
New: *ruleV3,
},
})
versions, err := store.GetAlertRuleVersions(context.Background(), ruleV3.GetKey())
require.NoError(t, err)
assert.Len(t, versions, 3)
diff := versions[0].Diff(versions[1], AlertRuleFieldsToIgnoreInDiff[:]...)
assert.ElementsMatch(t, []string{"RuleGroup", "NamespaceUID"}, diff.Paths())
})
}
// createAlertRule creates an alert rule in the database and returns it. // createAlertRule creates an alert rule in the database and returns it.
// If a generator is not specified, uniqueness of primary key is not guaranteed. // If a generator is not specified, uniqueness of primary key is not guaranteed.
func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule { func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule {

View File

@ -65,6 +65,29 @@ type alertRuleVersion struct {
Metadata string `xorm:"metadata"` Metadata string `xorm:"metadata"`
} }
// EqualSpec compares two alertRuleVersion objects for equality based on their specifications and returns true if they match.
// The comparison is very basic and can produce false-negative. Fields excluded: ID, ParentVersion, RestoredFrom, Version, Created and CreatedBy
func (a alertRuleVersion) EqualSpec(b alertRuleVersion) bool {
return a.RuleOrgID == b.RuleOrgID &&
a.RuleUID == b.RuleUID &&
a.RuleNamespaceUID == b.RuleNamespaceUID &&
a.RuleGroup == b.RuleGroup &&
a.RuleGroupIndex == b.RuleGroupIndex &&
a.Title == b.Title &&
a.Condition == b.Condition &&
a.Data == b.Data &&
a.IntervalSeconds == b.IntervalSeconds &&
a.Record == b.Record &&
a.NoDataState == b.NoDataState &&
a.ExecErrState == b.ExecErrState &&
a.For == b.For &&
a.Annotations == b.Annotations &&
a.Labels == b.Labels &&
a.IsPaused == b.IsPaused &&
a.NotificationSettings == b.NotificationSettings &&
a.Metadata == b.Metadata
}
func (a alertRuleVersion) TableName() string { func (a alertRuleVersion) TableName() string {
return "alert_rule_version" return "alert_rule_version"
} }