diff --git a/pkg/services/ngalert/models/instance.go b/pkg/services/ngalert/models/instance.go index 8cb42bfc42d..513ce1a3c85 100644 --- a/pkg/services/ngalert/models/instance.go +++ b/pkg/services/ngalert/models/instance.go @@ -9,6 +9,7 @@ import ( type AlertInstance struct { AlertInstanceKey `xorm:"extends"` Labels InstanceLabels + Annotations InstanceAnnotations CurrentState InstanceStateType CurrentReason string CurrentStateSince time.Time diff --git a/pkg/services/ngalert/models/instance_annotations.go b/pkg/services/ngalert/models/instance_annotations.go new file mode 100644 index 00000000000..7eee394ec33 --- /dev/null +++ b/pkg/services/ngalert/models/instance_annotations.go @@ -0,0 +1,34 @@ +package models + +import ( + "encoding/json" +) + +// InstanceAnnotations is an extension to map[string]string with methods +// for database serialization. +type InstanceAnnotations map[string]string + +// FromDB loads annotations stored in the database as JSON into InstanceAnnotations. +// FromDB is part of the xorm Conversion interface. +func (a *InstanceAnnotations) FromDB(b []byte) error { + if len(b) == 0 { + *a = nil + return nil + } + annotations := make(map[string]string) + err := json.Unmarshal(b, &annotations) + if err != nil { + return err + } + *a = annotations + return nil +} + +// ToDB serializes InstanceAnnotations to JSON for database storage. +// ToDB is part of the xorm Conversion interface. +func (a *InstanceAnnotations) ToDB() ([]byte, error) { + if a == nil || len(*a) == 0 { + return nil, nil + } + return json.Marshal(*a) +} diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index d2fa78db913..886f13d029a 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -32,9 +32,10 @@ import ( ) var ( - RuleMuts = AlertRuleMutators{} - NSMuts = NotificationSettingsMutators{} - RuleGen = &AlertRuleGenerator{ + RuleMuts = AlertRuleMutators{} + NSMuts = NotificationSettingsMutators{} + InstanceMuts = AlertInstanceMutators{} + RuleGen = &AlertRuleGenerator{ mutators: []AlertRuleMutator{ RuleMuts.WithUniqueUID(), RuleMuts.WithUniqueTitle(), }, @@ -928,6 +929,50 @@ func AlertInstanceGen(mutators ...AlertInstanceMutator) *AlertInstance { return instance } +type AlertInstanceMutators struct{} + +func (a AlertInstanceMutators) WithOrgID(orgID int64) AlertInstanceMutator { + return func(i *AlertInstance) { + i.RuleOrgID = orgID + } +} + +func (a AlertInstanceMutators) WithRuleUID(ruleUID string) AlertInstanceMutator { + return func(i *AlertInstance) { + i.RuleUID = ruleUID + } +} + +func (a AlertInstanceMutators) WithLabelsHash(hash string) AlertInstanceMutator { + return func(i *AlertInstance) { + i.LabelsHash = hash + } +} + +func (a AlertInstanceMutators) WithReason(reason string) AlertInstanceMutator { + return func(i *AlertInstance) { + i.CurrentReason = reason + } +} + +func (a AlertInstanceMutators) WithState(state InstanceStateType) AlertInstanceMutator { + return func(i *AlertInstance) { + i.CurrentState = state + } +} + +func (a AlertInstanceMutators) WithLabels(labels InstanceLabels) AlertInstanceMutator { + return func(i *AlertInstance) { + i.Labels = labels + } +} + +func (a AlertInstanceMutators) WithAnnotations(annotations InstanceAnnotations) AlertInstanceMutator { + return func(i *AlertInstance) { + i.Annotations = annotations + } +} + type Mutator[T any] func(*T) // CopyNotificationSettings creates a deep copy of NotificationSettings. diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index c1598d4aa3b..b52a8a5fd6c 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -341,6 +341,7 @@ func (c *cache) GetAlertInstances() []ngModels.AlertInstance { states = append(states, ngModels.AlertInstance{ AlertInstanceKey: key, Labels: ngModels.InstanceLabels(v2.Labels), + Annotations: v2.Annotations, CurrentState: ngModels.InstanceStateType(v2.State.String()), CurrentReason: v2.StateReason, LastEvalTime: v2.LastEvaluationTime, diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 401eeb079cf..4d6e066f2bf 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -184,8 +184,11 @@ func (st *Manager) Warm(ctx context.Context, orgReader OrgReader, rulesReader Ru continue } - // nil safety. - annotations := ruleForEntry.Annotations + // Use persisted annotations if available, otherwise fall back to rule annotations + annotations := entry.Annotations + if len(annotations) == 0 { + annotations = ruleForEntry.Annotations + } if annotations == nil { annotations = make(map[string]string) } diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 363d7aef78d..9dccbe52d2d 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -74,7 +74,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime), ResolvedAt: util.Pointer(evaluationTime), - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: rule.Annotations, // alert instance has no stored annotations, falls back to the rule annotations ResultFingerprint: data.Fingerprint(math.MaxUint64), }, { AlertRuleUID: rule.UID, @@ -87,7 +87,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, ResultFingerprint: data.Fingerprint(math.MaxUint64 - 1), }, { @@ -101,7 +101,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-3"}, ResultFingerprint: data.Fingerprint(0), }, { @@ -115,7 +115,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-4"}, ResultFingerprint: data.Fingerprint(1), }, { @@ -129,7 +129,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: nil, ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-5"}, ResultFingerprint: data.Fingerprint(2), }, } @@ -169,6 +169,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-2"}, ResultFingerprint: data.Fingerprint(math.MaxUint64 - 1).String(), }) @@ -187,6 +188,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-3"}, ResultFingerprint: data.Fingerprint(0).String(), }) @@ -205,6 +207,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-4"}, ResultFingerprint: data.Fingerprint(1).String(), }) @@ -223,6 +226,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: nil, ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-5"}, ResultFingerprint: data.Fingerprint(2).String(), }) @@ -241,6 +245,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: nil, ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-6"}, ResultFingerprint: data.Fingerprint(2).String(), }) @@ -1497,6 +1502,7 @@ func TestIntegrationStaleResultsHandler(t *testing.T) { }, CurrentState: models.InstanceStateNormal, Labels: labels1, + Annotations: rule.Annotations, LastEvalTime: lastEval, CurrentStateSince: lastEval, CurrentStateEnd: lastEval.Add(3 * interval), @@ -1512,6 +1518,7 @@ func TestIntegrationStaleResultsHandler(t *testing.T) { }, CurrentState: models.InstanceStateFiring, Labels: labels2, + Annotations: rule.Annotations, LastEvalTime: lastEval, CurrentStateSince: lastEval, CurrentStateEnd: lastEval.Add(3 * interval), @@ -1562,7 +1569,7 @@ func TestIntegrationStaleResultsHandler(t *testing.T) { LastSentAt: &lastEval, ResolvedAt: &lastEval, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: rule.Annotations, ResultFingerprint: data.Labels{"test1": "testValue1"}.Fingerprint(), }, }, @@ -1810,7 +1817,7 @@ func TestIntegrationDeleteStateByRuleUID(t *testing.T) { Labels: data.Labels{"test1": "testValue1"}, State: eval.Normal, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, { AlertRuleUID: rule.UID, @@ -1818,7 +1825,7 @@ func TestIntegrationDeleteStateByRuleUID(t *testing.T) { Labels: data.Labels{"test2": "testValue2"}, State: eval.Alerting, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, }, startingStateCacheCount: 2, @@ -1959,7 +1966,7 @@ func TestIntegrationResetStateByRuleUID(t *testing.T) { Labels: data.Labels{"test1": "testValue1"}, State: eval.Normal, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, { AlertRuleUID: rule.UID, @@ -1967,7 +1974,7 @@ func TestIntegrationResetStateByRuleUID(t *testing.T) { Labels: data.Labels{"test2": "testValue2"}, State: eval.Alerting, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, }, startingStateCacheCount: 2, diff --git a/pkg/services/ngalert/state/persister_sync.go b/pkg/services/ngalert/state/persister_sync.go index 8a22afb9ca3..707afed7069 100644 --- a/pkg/services/ngalert/state/persister_sync.go +++ b/pkg/services/ngalert/state/persister_sync.go @@ -89,6 +89,7 @@ func (a *SyncStatePersister) saveAlertStates(ctx context.Context, states ...Stat instance := ngModels.AlertInstance{ AlertInstanceKey: key, Labels: ngModels.InstanceLabels(s.Labels), + Annotations: s.Annotations, CurrentState: ngModels.InstanceStateType(s.State.State.String()), CurrentReason: s.StateReason, LastEvalTime: s.LastEvaluationTime, diff --git a/pkg/services/ngalert/state/persister_sync_rule.go b/pkg/services/ngalert/state/persister_sync_rule.go index a237d2983f2..f2c2b69a2a7 100644 --- a/pkg/services/ngalert/state/persister_sync_rule.go +++ b/pkg/services/ngalert/state/persister_sync_rule.go @@ -90,6 +90,7 @@ func (a *SyncRuleStatePersister) Sync(ctx context.Context, span trace.Span, rule instance := models.AlertInstance{ AlertInstanceKey: key, Labels: models.InstanceLabels(s.Labels), + Annotations: s.Annotations, CurrentState: models.InstanceStateType(s.State.State.String()), CurrentReason: s.StateReason, LastEvalTime: s.LastEvaluationTime, diff --git a/pkg/services/ngalert/store/instance_database.go b/pkg/services/ngalert/store/instance_database.go index 44822f72e99..a9d57d1ab8f 100644 --- a/pkg/services/ngalert/store/instance_database.go +++ b/pkg/services/ngalert/store/instance_database.go @@ -99,6 +99,10 @@ func (st InstanceDBStore) SaveAlertInstance(ctx context.Context, alertInstance m if err != nil { return err } + annotationsJSON, err := alertInstance.Annotations.ToDB() + if err != nil { + return err + } params := append(make([]any, 0), alertInstance.RuleOrgID, alertInstance.RuleUID, @@ -113,12 +117,13 @@ func (st InstanceDBStore) SaveAlertInstance(ctx context.Context, alertInstance m nullableTimeToUnix(alertInstance.ResolvedAt), nullableTimeToUnix(alertInstance.LastSentAt), alertInstance.ResultFingerprint, + annotationsJSON, ) upsertSQL := st.SQLStore.GetDialect().UpsertSQL( "alert_instance", []string{"rule_org_id", "rule_uid", "labels_hash"}, - []string{"rule_org_id", "rule_uid", "labels", "labels_hash", "current_state", "current_reason", "current_state_since", "current_state_end", "last_eval_time", "fired_at", "resolved_at", "last_sent_at", "result_fingerprint"}) + []string{"rule_org_id", "rule_uid", "labels", "labels_hash", "current_state", "current_reason", "current_state_since", "current_state_end", "last_eval_time", "fired_at", "resolved_at", "last_sent_at", "result_fingerprint", "annotations"}) _, err = sess.SQL(upsertSQL, params...).Query() if err != nil { return err @@ -359,10 +364,10 @@ func (st InstanceDBStore) insertInstancesBatch(sess *sqlstore.DBSession, batch [ query := strings.Builder{} placeholders := make([]string, 0, len(batch)) - args := make([]any, 0, len(batch)*12) + args := make([]any, 0, len(batch)*13) query.WriteString("INSERT INTO alert_instance ") - query.WriteString("(rule_org_id, rule_uid, labels, labels_hash, current_state, current_reason, current_state_since, current_state_end, last_eval_time, fired_at, resolved_at, last_sent_at) VALUES ") + query.WriteString("(rule_org_id, rule_uid, labels, labels_hash, current_state, current_reason, current_state_since, current_state_end, last_eval_time, fired_at, resolved_at, last_sent_at, annotations) VALUES ") for _, instance := range batch { if err := models.ValidateAlertInstance(instance); err != nil { @@ -376,7 +381,13 @@ func (st InstanceDBStore) insertInstancesBatch(sess *sqlstore.DBSession, batch [ continue } - placeholders = append(placeholders, "(?,?,?,?,?,?,?,?,?,?,?,?)") + annotationsJSON, err := instance.Annotations.ToDB() + if err != nil { + st.Logger.Warn("Skipping instance with invalid annotations", "err", err, "rule_uid", instance.RuleUID) + continue + } + + placeholders = append(placeholders, "(?,?,?,?,?,?,?,?,?,?,?,?,?)") args = append(args, instance.RuleOrgID, instance.RuleUID, @@ -390,6 +401,7 @@ func (st InstanceDBStore) insertInstancesBatch(sess *sqlstore.DBSession, batch [ nullableTimeToUnix(instance.FiredAt), nullableTimeToUnix(instance.ResolvedAt), nullableTimeToUnix(instance.LastSentAt), + annotationsJSON, ) } diff --git a/pkg/services/ngalert/store/instance_database_test.go b/pkg/services/ngalert/store/instance_database_test.go index ce0d9c795da..3cf264d4a36 100644 --- a/pkg/services/ngalert/store/instance_database_test.go +++ b/pkg/services/ngalert/store/instance_database_test.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" pb "github.com/grafana/grafana/pkg/services/ngalert/store/proto/v1" "github.com/grafana/grafana/pkg/services/ngalert/tests" - "github.com/grafana/grafana/pkg/util" ) const baseIntervalSeconds = 10 @@ -51,7 +50,15 @@ func TestIntegration_CompressedAlertRuleStateOperations(t *testing.T) { name: "can save and read alert rule state", setupInstances: func() []models.AlertInstance { return []models.AlertInstance{ - createAlertInstance(alertRule1.OrgID, alertRule1.UID, "labelsHash1", string(models.InstanceStateError), models.InstanceStateFiring), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule1.OrgID), + models.InstanceMuts.WithRuleUID(alertRule1.UID), + models.InstanceMuts.WithLabelsHash("labelsHash1"), + models.InstanceMuts.WithReason(string(models.InstanceStateError)), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), } }, listQuery: &models.ListAlertInstancesQuery{ @@ -67,8 +74,22 @@ func TestIntegration_CompressedAlertRuleStateOperations(t *testing.T) { name: "can save and read alert rule state with multiple instances", setupInstances: func() []models.AlertInstance { return []models.AlertInstance{ - createAlertInstance(alertRule1.OrgID, alertRule1.UID, "hash1", "", models.InstanceStateFiring), - createAlertInstance(alertRule1.OrgID, alertRule1.UID, "hash2", "", models.InstanceStateFiring), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule1.OrgID), + models.InstanceMuts.WithRuleUID(alertRule1.UID), + models.InstanceMuts.WithLabelsHash("hash1"), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule1.OrgID), + models.InstanceMuts.WithRuleUID(alertRule1.UID), + models.InstanceMuts.WithLabelsHash("hash2"), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), } }, listQuery: &models.ListAlertInstancesQuery{ @@ -109,19 +130,6 @@ func containsHash(t *testing.T, instances []*models.AlertInstance, hash string) require.Fail(t, fmt.Sprintf("%v does not contain an instance with hash %s", instances, hash)) } -func createAlertInstance(orgID int64, ruleUID, labelsHash, reason string, state models.InstanceStateType) models.AlertInstance { - return models.AlertInstance{ - AlertInstanceKey: models.AlertInstanceKey{ - RuleOrgID: orgID, - RuleUID: ruleUID, - LabelsHash: labelsHash, - }, - CurrentState: state, - CurrentReason: reason, - Labels: models.InstanceLabels{"label1": "value1"}, - } -} - func TestIntegrationAlertInstanceOperations(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -312,7 +320,10 @@ func TestIntegrationFullSync(t *testing.T) { instances := make([]models.AlertInstance, len(ruleUIDs)) for i, ruleUID := range ruleUIDs { - instances[i] = generateTestAlertInstance(orgID, ruleUID) + instances[i] = *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(orgID), + models.InstanceMuts.WithRuleUID(ruleUID), + ) } t.Run("Should do a proper full sync", func(t *testing.T) { @@ -356,7 +367,7 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should add new entries on sync", func(t *testing.T) { newRuleUID := "y" - err := ng.InstanceStore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize, nil) + err := ng.InstanceStore.FullSync(ctx, append(instances, *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(newRuleUID))), batchSize, nil) require.NoError(t, err) res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ @@ -381,7 +392,7 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should save all instances when batch size is bigger than 1", func(t *testing.T) { batchSize = 2 newRuleUID := "y" - err := ng.InstanceStore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize, nil) + err := ng.InstanceStore.FullSync(ctx, append(instances, *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(newRuleUID))), batchSize, nil) require.NoError(t, err) res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ @@ -406,8 +417,8 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should not fail when the instances are empty", func(t *testing.T) { // First, insert some data into the table. initialInstances := []models.AlertInstance{ - generateTestAlertInstance(orgID, "preexisting-1"), - generateTestAlertInstance(orgID, "preexisting-2"), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("preexisting-1")), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("preexisting-2")), } err := ng.InstanceStore.FullSync(ctx, initialInstances, 5, nil) require.NoError(t, err) @@ -439,9 +450,9 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should handle invalid instances by skipping them", func(t *testing.T) { // Create a batch with one valid and one invalid instance - validInstance := generateTestAlertInstance(orgID, "valid") + validInstance := *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("valid")) - invalidInstance := generateTestAlertInstance(orgID, "") + invalidInstance := *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("")) // Make the invalid instance actually invalid invalidInstance.RuleUID = "" @@ -460,8 +471,8 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should handle batchSize larger than the number of instances", func(t *testing.T) { // Insert a small number of instances but use a large batchSize smallSet := []models.AlertInstance{ - generateTestAlertInstance(orgID, "batch-test1"), - generateTestAlertInstance(orgID, "batch-test2"), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("batch-test1")), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("batch-test2")), } err := ng.InstanceStore.FullSync(ctx, smallSet, 100, nil) @@ -493,7 +504,7 @@ func TestIntegrationFullSync(t *testing.T) { largeCount := 300 largeSet := make([]models.AlertInstance, largeCount) for i := 0; i < largeCount; i++ { - largeSet[i] = generateTestAlertInstance(orgID, fmt.Sprintf("large-%d", i)) + largeSet[i] = *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(fmt.Sprintf("large-%d", i))) } err = ng.InstanceStore.FullSync(ctx, largeSet, 50, nil) @@ -520,7 +531,10 @@ func TestIntegrationFullSyncWithJitter(t *testing.T) { instances := make([]models.AlertInstance, len(ruleUIDs)) for i, ruleUID := range ruleUIDs { - instances[i] = generateTestAlertInstance(orgID, ruleUID) + instances[i] = *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(orgID), + models.InstanceMuts.WithRuleUID(ruleUID), + ) } // Simple jitter function for testing @@ -565,7 +579,7 @@ func TestIntegrationFullSyncWithJitter(t *testing.T) { t.Run("Should handle zero delays (immediate execution)", func(t *testing.T) { testInstances := make([]models.AlertInstance, 2) for i := 0; i < 2; i++ { - testInstances[i] = generateTestAlertInstance(orgID, fmt.Sprintf("immediate-%d", i)) + testInstances[i] = *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(fmt.Sprintf("immediate-%d", i))) } // Function that returns zero delays @@ -592,7 +606,7 @@ func TestIntegrationFullSyncWithJitter(t *testing.T) { t.Run("Should execute jitter delays correctly and save data", func(t *testing.T) { testInstances := make([]models.AlertInstance, 4) for i := 0; i < 4; i++ { - testInstances[i] = generateTestAlertInstance(orgID, fmt.Sprintf("jitter-test-%d", i)) + testInstances[i] = *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(fmt.Sprintf("jitter-test-%d", i))) } // Track jitter function calls @@ -652,11 +666,16 @@ func TestIntegration_ProtoInstanceDBStore_VerifyCompressedData(t *testing.T) { alertRule := tests.CreateTestAlertRule(t, ctx, dbstore, 60, 1) - labelsHash := "hash1" - reason := "reason" - state := models.InstanceStateFiring instances := []models.AlertInstance{ - createAlertInstance(alertRule.OrgID, alertRule.UID, labelsHash, reason, state), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule.OrgID), + models.InstanceMuts.WithRuleUID(alertRule.UID), + models.InstanceMuts.WithLabelsHash("hash1"), + models.InstanceMuts.WithReason("reason"), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), } err := ng.InstanceStore.SaveAlertInstancesForRule(ctx, alertRule.GetKeyWithGroup(), instances) @@ -704,25 +723,3 @@ func decompressAlertInstances(compressed []byte) ([]*pb.AlertInstance, error) { return instances.Instances, nil } - -func generateTestAlertInstance(orgID int64, ruleID string) models.AlertInstance { - return models.AlertInstance{ - AlertInstanceKey: models.AlertInstanceKey{ - RuleOrgID: orgID, - RuleUID: ruleID, - LabelsHash: "abc", - }, - CurrentState: models.InstanceStateFiring, - Labels: map[string]string{ - "hello": "world", - }, - ResultFingerprint: "abc", - CurrentStateEnd: time.Now(), - CurrentStateSince: time.Now(), - LastEvalTime: time.Now(), - LastSentAt: util.Pointer(time.Now()), - FiredAt: util.Pointer(time.Now()), - ResolvedAt: util.Pointer(time.Now()), - CurrentReason: "abc", - } -} diff --git a/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go b/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go index 4c32cc2f879..a494c767a45 100644 --- a/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go +++ b/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go @@ -35,6 +35,7 @@ type AlertInstance struct { ResolvedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=resolved_at,json=resolvedAt,proto3" json:"resolved_at,omitempty"` ResultFingerprint string `protobuf:"bytes,10,opt,name=result_fingerprint,json=resultFingerprint,proto3" json:"result_fingerprint,omitempty"` FiredAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=fired_at,json=firedAt,proto3" json:"fired_at,omitempty"` + Annotations map[string]string `protobuf:"bytes,12,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -146,6 +147,13 @@ func (x *AlertInstance) GetFiredAt() *timestamppb.Timestamp { return nil } +func (x *AlertInstance) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + type AlertInstances struct { state protoimpl.MessageState `protogen:"open.v1"` Instances []*AlertInstance `protobuf:"bytes,1,rep,name=instances,proto3" json:"instances,omitempty"` @@ -197,7 +205,7 @@ var file_alert_rule_state_proto_rawDesc = string([]byte{ 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb3, 0x05, 0x0a, 0x0d, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc7, 0x06, 0x0a, 0x0d, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x43, @@ -237,20 +245,29 @@ var file_alert_rule_state_proto_rawDesc = string([]byte{ 0x35, 0x0a, 0x08, 0x66, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x66, - 0x69, 0x72, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x4f, 0x0a, 0x0e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, - 0x63, 0x65, 0x73, 0x12, 0x3d, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, - 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x73, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, - 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x6e, 0x67, - 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x72, 0x65, 0x64, 0x41, 0x74, 0x12, 0x52, 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x6e, 0x67, + 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x41, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x61, + 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3e, 0x0a, 0x10, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4f, 0x0a, 0x0e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x3d, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6e, 0x67, 0x61, + 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, + 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x09, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, + 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2f, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -265,27 +282,29 @@ func file_alert_rule_state_proto_rawDescGZIP() []byte { return file_alert_rule_state_proto_rawDescData } -var file_alert_rule_state_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_alert_rule_state_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_alert_rule_state_proto_goTypes = []any{ (*AlertInstance)(nil), // 0: ngalert.store.v1.AlertInstance (*AlertInstances)(nil), // 1: ngalert.store.v1.AlertInstances nil, // 2: ngalert.store.v1.AlertInstance.LabelsEntry - (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp + nil, // 3: ngalert.store.v1.AlertInstance.AnnotationsEntry + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp } var file_alert_rule_state_proto_depIdxs = []int32{ 2, // 0: ngalert.store.v1.AlertInstance.labels:type_name -> ngalert.store.v1.AlertInstance.LabelsEntry - 3, // 1: ngalert.store.v1.AlertInstance.current_state_since:type_name -> google.protobuf.Timestamp - 3, // 2: ngalert.store.v1.AlertInstance.current_state_end:type_name -> google.protobuf.Timestamp - 3, // 3: ngalert.store.v1.AlertInstance.last_eval_time:type_name -> google.protobuf.Timestamp - 3, // 4: ngalert.store.v1.AlertInstance.last_sent_at:type_name -> google.protobuf.Timestamp - 3, // 5: ngalert.store.v1.AlertInstance.resolved_at:type_name -> google.protobuf.Timestamp - 3, // 6: ngalert.store.v1.AlertInstance.fired_at:type_name -> google.protobuf.Timestamp - 0, // 7: ngalert.store.v1.AlertInstances.instances:type_name -> ngalert.store.v1.AlertInstance - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 4, // 1: ngalert.store.v1.AlertInstance.current_state_since:type_name -> google.protobuf.Timestamp + 4, // 2: ngalert.store.v1.AlertInstance.current_state_end:type_name -> google.protobuf.Timestamp + 4, // 3: ngalert.store.v1.AlertInstance.last_eval_time:type_name -> google.protobuf.Timestamp + 4, // 4: ngalert.store.v1.AlertInstance.last_sent_at:type_name -> google.protobuf.Timestamp + 4, // 5: ngalert.store.v1.AlertInstance.resolved_at:type_name -> google.protobuf.Timestamp + 4, // 6: ngalert.store.v1.AlertInstance.fired_at:type_name -> google.protobuf.Timestamp + 3, // 7: ngalert.store.v1.AlertInstance.annotations:type_name -> ngalert.store.v1.AlertInstance.AnnotationsEntry + 0, // 8: ngalert.store.v1.AlertInstances.instances:type_name -> ngalert.store.v1.AlertInstance + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_alert_rule_state_proto_init() } @@ -299,7 +318,7 @@ func file_alert_rule_state_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_alert_rule_state_proto_rawDesc), len(file_alert_rule_state_proto_rawDesc)), NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto b/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto index e80ac93cd09..5b48b422c27 100644 --- a/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto +++ b/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto @@ -18,6 +18,7 @@ message AlertInstance { google.protobuf.Timestamp resolved_at = 9; string result_fingerprint = 10; google.protobuf.Timestamp fired_at = 11; + map annotations = 12; } message AlertInstances { diff --git a/pkg/services/ngalert/store/proto_instance_database.go b/pkg/services/ngalert/store/proto_instance_database.go index a9450d0851f..04ed45994e0 100644 --- a/pkg/services/ngalert/store/proto_instance_database.go +++ b/pkg/services/ngalert/store/proto_instance_database.go @@ -188,6 +188,7 @@ func alertInstanceModelToProto(modelInstance models.AlertInstance) *pb.AlertInst return &pb.AlertInstance{ Labels: modelInstance.Labels, LabelsHash: modelInstance.LabelsHash, + Annotations: modelInstance.Annotations, CurrentState: string(modelInstance.CurrentState), CurrentStateSince: timestamppb.New(modelInstance.CurrentStateSince), CurrentStateEnd: timestamppb.New(modelInstance.CurrentStateEnd), @@ -255,6 +256,7 @@ func alertInstanceProtoToModel(ruleUID string, ruleOrgID int64, protoInstance *p LabelsHash: protoInstance.LabelsHash, }, Labels: protoInstance.Labels, + Annotations: protoInstance.Annotations, CurrentState: models.InstanceStateType(protoInstance.CurrentState), CurrentStateSince: protoInstance.CurrentStateSince.AsTime(), CurrentStateEnd: protoInstance.CurrentStateEnd.AsTime(), diff --git a/pkg/services/ngalert/store/proto_instance_database_test.go b/pkg/services/ngalert/store/proto_instance_database_test.go index c98923995dd..b8e809a88e8 100644 --- a/pkg/services/ngalert/store/proto_instance_database_test.go +++ b/pkg/services/ngalert/store/proto_instance_database_test.go @@ -19,6 +19,7 @@ func TestAlertInstanceModelToProto(t *testing.T) { lastSentAt := currentStateSince.Add(-2 * time.Minute) firedAt := currentStateSince.Add(-2 * time.Minute) resolvedAt := currentStateSince.Add(-3 * time.Minute) + annotations := map[string]string{"summary": "value", "team": "alerting"} tests := []struct { name string @@ -28,7 +29,8 @@ func TestAlertInstanceModelToProto(t *testing.T) { { name: "valid instance", input: models.AlertInstance{ - Labels: map[string]string{"key": "value"}, + Labels: map[string]string{"key": "value"}, + Annotations: annotations, AlertInstanceKey: models.AlertInstanceKey{ RuleUID: "rule-uid-1", RuleOrgID: 1, @@ -46,6 +48,7 @@ func TestAlertInstanceModelToProto(t *testing.T) { }, expected: &pb.AlertInstance{ Labels: map[string]string{"key": "value"}, + Annotations: annotations, LabelsHash: "hash123", CurrentState: "Alerting", CurrentStateSince: timestamppb.New(currentStateSince), @@ -75,6 +78,7 @@ func TestAlertInstanceProtoToModel(t *testing.T) { lastSentAt := currentStateSince.Add(-2 * time.Minute).UTC() firedAt := currentStateSince.Add(-2 * time.Minute).UTC() resolvedAt := currentStateSince.Add(-3 * time.Minute).UTC() + annotations := map[string]string{"summary": "value", "team": "alerting"} ruleUID := "rule-uid-1" orgID := int64(1) @@ -87,6 +91,7 @@ func TestAlertInstanceProtoToModel(t *testing.T) { name: "valid instance", input: &pb.AlertInstance{ Labels: map[string]string{"key": "value"}, + Annotations: annotations, LabelsHash: "hash123", CurrentState: "Alerting", CurrentStateSince: timestamppb.New(currentStateSince), @@ -98,7 +103,8 @@ func TestAlertInstanceProtoToModel(t *testing.T) { ResultFingerprint: "fingerprint", }, expected: &models.AlertInstance{ - Labels: map[string]string{"key": "value"}, + Labels: map[string]string{"key": "value"}, + Annotations: annotations, AlertInstanceKey: models.AlertInstanceKey{ RuleUID: ruleUID, RuleOrgID: orgID, @@ -132,7 +138,7 @@ func TestModelAlertInstanceMatchesProtobuf(t *testing.T) { // and update them accordingly. t.Run("when AlertInstance model changes", func(t *testing.T) { modelType := reflect.TypeOf(models.AlertInstance{}) - require.Equal(t, 11, modelType.NumField(), "AlertInstance model has changed, update the protobuf") + require.Equal(t, 12, modelType.NumField(), "AlertInstance model has changed, update the protobuf") }) } @@ -142,6 +148,7 @@ func TestCompressAndDecompressAlertInstances(t *testing.T) { alertInstances := []*pb.AlertInstance{ { Labels: map[string]string{"label-1": "value-1"}, + Annotations: map[string]string{"anno-1": "value-1"}, LabelsHash: "hash-1", CurrentState: "normal", CurrentStateSince: timestamppb.New(now), @@ -154,6 +161,7 @@ func TestCompressAndDecompressAlertInstances(t *testing.T) { }, { Labels: map[string]string{"label-2": "value-2"}, + Annotations: map[string]string{"anno-2": "value-2"}, LabelsHash: "hash-2", CurrentState: "firing", CurrentStateSince: timestamppb.New(now), @@ -185,6 +193,7 @@ func TestConvertAndCompressAlertInstances(t *testing.T) { LabelsHash: "hash-1", }, Labels: map[string]string{"label-1": "value-1"}, + Annotations: map[string]string{"anno-1": "value-1"}, CurrentState: models.InstanceStateFiring, CurrentStateSince: now, CurrentStateEnd: now.Add(time.Hour), @@ -202,6 +211,7 @@ func TestConvertAndCompressAlertInstances(t *testing.T) { LabelsHash: "hash-2", }, Labels: map[string]string{"label-2": "value-2"}, + Annotations: map[string]string{"anno-2": "value-2"}, CurrentState: models.InstanceStateNormal, CurrentStateSince: now, CurrentStateEnd: now.Add(time.Hour), @@ -227,6 +237,7 @@ func TestConvertAndCompressAlertInstances(t *testing.T) { for i, protoInstance := range decompressedInstances { modelInstance := alertInstanceProtoToModel("rule-uid-1", 1, protoInstance) require.Equal(t, modelInstances[i].Labels, modelInstance.Labels) + require.Equal(t, modelInstances[i].Annotations, modelInstance.Annotations) require.Equal(t, modelInstances[i].CurrentState, modelInstance.CurrentState) require.Equal(t, modelInstances[i].LabelsHash, modelInstance.LabelsHash) require.Equal(t, modelInstances[i].ResultFingerprint, modelInstance.ResultFingerprint) diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 4fc4e1df131..e713525c0f6 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -163,5 +163,7 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { ualert.AddAlertRuleGroupIndexMigration(mg) + ualert.AddStateAnnotationsColumn(mg) + ualert.CollateBinAlertRuleGroup(mg) } diff --git a/pkg/services/sqlstore/migrations/ualert/state_annotations_mig.go b/pkg/services/sqlstore/migrations/ualert/state_annotations_mig.go new file mode 100644 index 00000000000..e4f599c9f44 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/state_annotations_mig.go @@ -0,0 +1,12 @@ +package ualert + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// AddStateAnnotationsColumn adds annotations column to alert_instance +func AddStateAnnotationsColumn(mg *migrator.Migrator) { + mg.AddMigration("add annotations column to alert_instance table", migrator.NewAddColumnMigration(migrator.Table{Name: "alert_instance"}, &migrator.Column{ + Name: "annotations", + Type: migrator.DB_Text, + Nullable: true, + })) +} diff --git a/pkg/storage/unified/resourcepb/search.pb.go b/pkg/storage/unified/resourcepb/search.pb.go index 91f5aadfa97..459e9aa3429 100644 --- a/pkg/storage/unified/resourcepb/search.pb.go +++ b/pkg/storage/unified/resourcepb/search.pb.go @@ -388,7 +388,7 @@ func (x *ResourceSearchResponse) GetFacet() map[string]*ResourceSearchResponse_F type RebuildIndexesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Namespace (tenant) + // Namespace (tenant) must be the same as all keys' namespace Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` // List of ResourceKeys (Namespace + Group + Resource) Keys []*ResourceKey `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`