Alerting: Store instance annotations in alert rule state (#114975)

Alerting: Store annotations in alert instance state
This commit is contained in:
Alexander Akhmetov
2025-12-09 13:52:42 +01:00
committed by GitHub
parent 6746c978b4
commit c59d5d1c8e
17 changed files with 258 additions and 109 deletions
+1
View File
@@ -9,6 +9,7 @@ import (
type AlertInstance struct {
AlertInstanceKey `xorm:"extends"`
Labels InstanceLabels
Annotations InstanceAnnotations
CurrentState InstanceStateType
CurrentReason string
CurrentStateSince time.Time
@@ -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)
}
+48 -3
View File
@@ -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.
+1
View File
@@ -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,
+5 -2
View File
@@ -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)
}
+17 -10
View File
@@ -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,
@@ -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,
@@ -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,
@@ -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,
)
}
@@ -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",
}
}
@@ -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,
},
@@ -18,6 +18,7 @@ message AlertInstance {
google.protobuf.Timestamp resolved_at = 9;
string result_fingerprint = 10;
google.protobuf.Timestamp fired_at = 11;
map<string, string> annotations = 12;
}
message AlertInstances {
@@ -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(),
@@ -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)
@@ -163,5 +163,7 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) {
ualert.AddAlertRuleGroupIndexMigration(mg)
ualert.AddStateAnnotationsColumn(mg)
ualert.CollateBinAlertRuleGroup(mg)
}
@@ -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,
}))
}
+1 -1
View File
@@ -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"`