diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index cdbc2a35b20..743d2295389 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -266,7 +266,9 @@ func NewUserUID(requester interface{ GetIdentifier() string }) *UserUID { // AlertRule is the model for alert rules in unified alerting. type AlertRule struct { - ID int64 + ID int64 + // Uniquely identifies alert rule across all organizations and time + GUID string OrgID int64 Title string Condition string @@ -708,6 +710,7 @@ func (alertRule *AlertRule) Copy() *AlertRule { } result := AlertRule{ ID: alertRule.ID, + GUID: alertRule.GUID, OrgID: alertRule.OrgID, Title: alertRule.Title, Condition: alertRule.Condition, @@ -940,6 +943,10 @@ func PatchPartialAlertRule(existingRule *AlertRule, ruleToPatch *AlertRuleWithOp if !ruleToPatch.HasEditorSettings { ruleToPatch.Metadata.EditorSettings = existingRule.Metadata.EditorSettings } + + if ruleToPatch.GUID == "" { + ruleToPatch.GUID = existingRule.GUID + } } func ValidateRuleGroupInterval(intervalSeconds, baseIntervalSeconds int64) error { diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index 84cbed48175..43d5092842a 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -418,6 +418,15 @@ func TestDiff(t *testing.T) { assert.Equal(t, rule2.ID, diff[0].Right.Int()) difCnt++ } + + if rule1.GUID != rule2.GUID { + diff := diffs.GetDiffsForField("GUID") + assert.Len(t, diff, 1) + assert.Equal(t, rule1.GUID, diff[0].Left.String()) + assert.Equal(t, rule2.GUID, diff[0].Right.String()) + difCnt++ + } + if rule1.OrgID != rule2.OrgID { diff := diffs.GetDiffsForField("OrgID") assert.Len(t, diff, 1) diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 6d553057000..eb9d838f8df 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -11,6 +11,7 @@ import ( "time" "github.com/go-openapi/strfmt" + "github.com/google/uuid" alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/grafana-plugin-sdk-go/data" amv2 "github.com/prometheus/alertmanager/api/v2/models" @@ -103,6 +104,7 @@ func (g *AlertRuleGenerator) Generate() AlertRule { rule := AlertRule{ ID: 0, + GUID: uuid.NewString(), OrgID: rand.Int63n(1500) + 1, // Prevent OrgID=0 as this does not pass alert rule validation. Title: fmt.Sprintf("title-%s", util.GenerateShortUID()), Condition: "A", @@ -584,6 +586,12 @@ func (a *AlertRuleMutators) WithMetadata(meta AlertRuleMetadata) AlertRuleMutato } } +func (a AlertRuleMutators) WithGUID(guid string) AlertRuleMutator { + return func(r *AlertRule) { + r.GUID = guid + } +} + func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels { count := max if min > max { diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go index fb779c346ea..2a52f67b264 100644 --- a/pkg/services/ngalert/schedule/registry_test.go +++ b/pkg/services/ngalert/schedule/registry_test.go @@ -151,13 +151,14 @@ func TestRuleWithFolderFingerprint(t *testing.T) { f2 := ruleWithFolder{rule: rule, folderTitle: uuid.NewString()}.Fingerprint() require.NotEqual(t, f, f2) }) - t.Run("Version, Updated, IntervalSeconds and Annotations should be excluded from fingerprint", func(t *testing.T) { + t.Run("Version, Updated, IntervalSeconds, GUID and Annotations should be excluded from fingerprint", func(t *testing.T) { cp := models.CopyRule(rule) cp.Version++ cp.Updated = cp.Updated.Add(1 * time.Second) cp.IntervalSeconds++ cp.Annotations = make(map[string]string) cp.Annotations["test"] = "test" + cp.GUID = uuid.NewString() f2 := ruleWithFolder{rule: cp, folderTitle: title}.Fingerprint() require.Equal(t, f, f2) @@ -264,6 +265,7 @@ func TestRuleWithFolderFingerprint(t *testing.T) { "Annotations": {}, "ID": {}, "OrgID": {}, + "GUID": {}, } tp := reflect.TypeOf(rule).Elem() diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index c95c8de835f..b56c660876b 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -261,6 +261,11 @@ func (st DBstore) InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, if err != nil { return fmt.Errorf("failed to convert alert rule %q to storage model: %w", r.Title, err) } + + // assign unique identifier that will identify resource across space and time. The probability of collision is so low that we do not need to check for uniqueness. + // The unique keys will ensure uniqueness in rule and versions tables + converted.GUID = uuid.NewString() + newRules = append(newRules, converted) ruleVersions = append(ruleVersions, alertRuleToAlertRuleVersion(converted)) } @@ -313,6 +318,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, r := rules[i] r.New.ID = r.Existing.ID r.New.Version = r.Existing.Version // xorm will take care of increasing it (see https://xorm.io/docs/chapter-06/1.lock/) + r.New.GUID = r.Existing.GUID if err := st.validateAlertRule(r.New); err != nil { return err } diff --git a/pkg/services/ngalert/store/compat.go b/pkg/services/ngalert/store/compat.go index cdb85d526c6..8021233ee93 100644 --- a/pkg/services/ngalert/store/compat.go +++ b/pkg/services/ngalert/store/compat.go @@ -20,6 +20,7 @@ func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, e result := models.AlertRule{ ID: ar.ID, OrgID: ar.OrgID, + GUID: ar.GUID, Title: ar.Title, Condition: ar.Condition, Data: data, @@ -107,6 +108,7 @@ func parseNotificationSettings(s string) ([]models.NotificationSettings, error) func alertRuleFromModelsAlertRule(ar models.AlertRule) (alertRule, error) { result := alertRule{ ID: ar.ID, + GUID: ar.GUID, OrgID: ar.OrgID, Title: ar.Title, Condition: ar.Condition, @@ -180,6 +182,7 @@ func alertRuleFromModelsAlertRule(ar models.AlertRule) (alertRule, error) { func alertRuleToAlertRuleVersion(rule alertRule) alertRuleVersion { return alertRuleVersion{ RuleOrgID: rule.OrgID, + RuleGUID: rule.GUID, RuleUID: rule.UID, RuleNamespaceUID: rule.NamespaceUID, RuleGroup: rule.RuleGroup, @@ -208,6 +211,7 @@ func alertRuleToAlertRuleVersion(rule alertRule) alertRuleVersion { func alertRuleVersionToAlertRule(version alertRuleVersion) alertRule { return alertRule{ ID: version.ID, + GUID: version.RuleGUID, OrgID: version.RuleOrgID, Title: version.Title, Condition: version.Condition, diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index 7381df4397a..dbfeb556ce4 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -169,6 +169,12 @@ func TestCalculateChanges(t *testing.T) { r.For = 0 }, }, + { + name: "GUID is empty", + mutator: func(r *models.AlertRule) { + r.GUID = "" + }, + }, } dbRule := gen.With(gen.WithOrgID(orgId)).GenerateRef() diff --git a/pkg/services/ngalert/store/models.go b/pkg/services/ngalert/store/models.go index ed9456036c2..8c5d44a9d8e 100644 --- a/pkg/services/ngalert/store/models.go +++ b/pkg/services/ngalert/store/models.go @@ -4,8 +4,9 @@ import "time" // alertRule represents a record in alert_rule table type alertRule struct { - ID int64 `xorm:"pk autoincr 'id'"` - OrgID int64 `xorm:"org_id"` + ID int64 `xorm:"pk autoincr 'id'"` + GUID string `xorm:"guid"` + OrgID int64 `xorm:"org_id"` Title string Condition string Data string @@ -38,6 +39,7 @@ func (a alertRule) TableName() string { type alertRuleVersion struct { ID int64 `xorm:"pk autoincr 'id'"` RuleOrgID int64 `xorm:"rule_org_id"` + RuleGUID string `xorm:"rule_guid"` RuleUID string `xorm:"rule_uid"` RuleNamespaceUID string `xorm:"rule_namespace_uid"` RuleGroup string @@ -69,6 +71,7 @@ type alertRuleVersion struct { // 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.RuleGUID == b.RuleGUID && a.RuleUID == b.RuleUID && a.RuleNamespaceUID == b.RuleNamespaceUID && a.RuleGroup == b.RuleGroup && diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 40704dac838..47cc25c2888 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -145,4 +145,6 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { ualert.AddAlertRuleUpdatedByMigration(mg) ualert.AddAlertRuleStateTable(mg) + + ualert.AddAlertRuleGuidMigration(mg) } diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_version_guid_mig.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_version_guid_mig.go new file mode 100644 index 00000000000..600167f4829 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_version_guid_mig.go @@ -0,0 +1,83 @@ +package ualert + +import ( + "github.com/google/uuid" + + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "xorm.io/xorm" +) + +// AddAlertRuleGuidMigration sets up migrations for adding and managing GUID columns in alert_rule and alert_rule_version tables. +func AddAlertRuleGuidMigration(mg *migrator.Migrator) { + alertRuleVersion := migrator.Table{Name: "alert_rule_version"} + alertRule := migrator.Table{Name: "alert_rule"} + mg.AddMigration("add guid column to alert_rule table", migrator.NewAddColumnMigration(alertRule, &migrator.Column{ + Name: "guid", + Type: migrator.DB_Varchar, + Length: 36, + Nullable: false, + Default: "''", + })) + mg.AddMigration("add rule_guid column to alert_rule_version table", migrator.NewAddColumnMigration(alertRuleVersion, &migrator.Column{ + Name: "rule_guid", + Type: migrator.DB_Varchar, + Length: 36, + Nullable: false, + Default: "''", + })) + mg.AddMigration("drop index in alert_rule_version table on rule_org_id, rule_uid and version columns", migrator.NewDropIndexMigration(alertRuleVersion, alertRuleVersionUDX_OrgIdRuleUIDVersion)) + + mg.AddMigration("populate rule guid in alert rule table", &setRuleGuidMigration{}) + + mg.AddMigration("add index in alert_rule_version table on rule_org_id, rule_uid, rule_guid and version columns", + migrator.NewAddIndexMigration(alertRuleVersion, + &migrator.Index{Cols: []string{"rule_org_id", "rule_uid", "rule_guid", "version"}, Type: migrator.UniqueIndex}, + ), + ) + + mg.AddMigration("add index in alert_rule_version table on rule_guid and version columns", + migrator.NewAddIndexMigration(alertRuleVersion, + &migrator.Index{Cols: []string{"rule_guid", "version"}, Type: migrator.UniqueIndex}, + ), + ) + + mg.AddMigration("add index in alert_rule table on guid columns", + migrator.NewAddIndexMigration(alertRule, + &migrator.Index{Cols: []string{"guid"}, Type: migrator.UniqueIndex}, + )) +} + +type setRuleGuidMigration struct { + migrator.MigrationBase +} + +var _ migrator.CodeMigration = (*setRuleGuidMigration)(nil) + +func (c setRuleGuidMigration) SQL(migrator.Dialect) string { + return codeMigration +} + +func (c setRuleGuidMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + var results []string + if err := sess.SQL("SELECT uid FROM alert_rule").Find(&results); err != nil { + return err + } + if len(results) == 0 { + mg.Logger.Debug("no rules found") + return nil + } + for _, uid := range results { + u := uuid.NewString() + _, err := sess.Exec("UPDATE alert_rule_version SET rule_guid = ? WHERE rule_uid = ?", u, uid) + if err != nil { + mg.Logger.Error("Failed to update alert_rule_version table", "error", err) + return err + } + _, err = sess.Exec("UPDATE alert_rule SET guid = ? WHERE uid = ?", u, uid) + if err != nil { + mg.Logger.Error("Failed to update alert_rule table", "error", err) + return err + } + } + return nil +} diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index 1ef0587efe3..023deb2a3cb 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -302,6 +302,8 @@ func addAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) UPDATE alert_rule SET is_paused = false;`)) } +var alertRuleVersionUDX_OrgIdRuleUIDVersion = &migrator.Index{Cols: []string{"rule_org_id", "rule_uid", "version"}, Type: migrator.UniqueIndex} + func addAlertRuleVersionMigrations(mg *migrator.Migrator) { // DO NOT EDIT alertRuleVersion := migrator.Table{ @@ -325,12 +327,12 @@ func addAlertRuleVersionMigrations(mg *migrator.Migrator) { {Name: "exec_err_state", Type: migrator.DB_NVarchar, Length: 15, Nullable: false, Default: "'Alerting'"}, }, Indices: []*migrator.Index{ - {Cols: []string{"rule_org_id", "rule_uid", "version"}, Type: migrator.UniqueIndex}, + alertRuleVersionUDX_OrgIdRuleUIDVersion, {Cols: []string{"rule_org_id", "rule_namespace_uid", "rule_group"}, Type: migrator.IndexType}, }, } mg.AddMigration("create alert_rule_version table", migrator.NewAddTableMigration(alertRuleVersion)) - mg.AddMigration("add index in alert_rule_version table on rule_org_id, rule_uid and version columns", migrator.NewAddIndexMigration(alertRuleVersion, alertRuleVersion.Indices[0])) + mg.AddMigration("add index in alert_rule_version table on rule_org_id, rule_uid and version columns", migrator.NewAddIndexMigration(alertRuleVersion, alertRuleVersionUDX_OrgIdRuleUIDVersion)) mg.AddMigration("add index in alert_rule_version table on rule_org_id, rule_namespace_uid and rule_group columns", migrator.NewAddIndexMigration(alertRuleVersion, alertRuleVersion.Indices[1])) mg.AddMigration("alter alert_rule_version table data column to mediumtext in mysql", migrator.NewRawSQLMigration("").