Alerting: Update Alert Rule to use int64 for MissingSeriesEvalsToResolve (#109306)

This commit is contained in:
Moustafa Baiou 2025-08-06 21:45:48 -04:00 committed by GitHub
parent e36402a121
commit 16f8359d35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 50 additions and 50 deletions

View File

@ -403,7 +403,7 @@ func TestIntegrationProvisioningApi(t *testing.T) {
}) })
t.Run("PUT without MissingSeriesEvalsToResolve clears the field", func(t *testing.T) { t.Run("PUT without MissingSeriesEvalsToResolve clears the field", func(t *testing.T) {
oldValue := util.Pointer(5) oldValue := util.Pointer[int64](5)
sut := createProvisioningSrvSut(t) sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx() rc := createTestRequestCtx()
rule := createTestAlertRule("rule", 1) rule := createTestAlertRule("rule", 1)
@ -420,8 +420,8 @@ func TestIntegrationProvisioningApi(t *testing.T) {
}) })
t.Run("PUT with MissingSeriesEvalsToResolve updates the value", func(t *testing.T) { t.Run("PUT with MissingSeriesEvalsToResolve updates the value", func(t *testing.T) {
oldValue := util.Pointer(5) oldValue := util.Pointer[int64](5)
newValue := util.Pointer(10) newValue := util.Pointer[int64](10)
sut := createProvisioningSrvSut(t) sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx() rc := createTestRequestCtx()
rule := createTestAlertRule("rule", 1) rule := createTestAlertRule("rule", 1)
@ -657,12 +657,12 @@ func TestIntegrationProvisioningApi(t *testing.T) {
require.Nil(t, updated.Rules[0].MissingSeriesEvalsToResolve) require.Nil(t, updated.Rules[0].MissingSeriesEvalsToResolve)
// Put the same group with a new value // Put the same group with a new value
group.Rules[0].MissingSeriesEvalsToResolve = util.Pointer(5) group.Rules[0].MissingSeriesEvalsToResolve = util.Pointer[int64](5)
response = sut.RoutePutAlertRuleGroup(&rc, group, "folder-uid", group.Title) response = sut.RoutePutAlertRuleGroup(&rc, group, "folder-uid", group.Title)
require.Equal(t, 200, response.Status()) require.Equal(t, 200, response.Status())
updated = deserializeRuleGroup(t, response.Body()) updated = deserializeRuleGroup(t, response.Body())
require.NotNil(t, updated.Rules[0].MissingSeriesEvalsToResolve) require.NotNil(t, updated.Rules[0].MissingSeriesEvalsToResolve)
require.Equal(t, 5, *updated.Rules[0].MissingSeriesEvalsToResolve) require.Equal(t, int64(5), *updated.Rules[0].MissingSeriesEvalsToResolve)
// Reset the value again // Reset the value again
group.Rules[0].MissingSeriesEvalsToResolve = nil group.Rules[0].MissingSeriesEvalsToResolve = nil

View File

@ -593,7 +593,7 @@ type PostableGrafanaRule struct {
// If set to 0, the value is reset to the default. // If set to 0, the value is reset to the default.
// required: false // required: false
// example: 3 // example: 3
MissingSeriesEvalsToResolve *int `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty"` MissingSeriesEvalsToResolve *int64 `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty"`
} }
// swagger:model // swagger:model
@ -616,7 +616,7 @@ type GettableGrafanaRule struct {
Record *Record `json:"record,omitempty" yaml:"record,omitempty"` Record *Record `json:"record,omitempty" yaml:"record,omitempty"`
Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"`
GUID string `json:"guid" yaml:"guid"` GUID string `json:"guid" yaml:"guid"`
MissingSeriesEvalsToResolve *int `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty"` MissingSeriesEvalsToResolve *int64 `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty"`
} }
// UserInfo represents user-related information, including a unique identifier and a name. // UserInfo represents user-related information, including a unique identifier and a name.

View File

@ -174,7 +174,7 @@ type ProvisionedAlertRule struct {
// example: {"metric":"grafana_alerts_ratio", "from":"A"} // example: {"metric":"grafana_alerts_ratio", "from":"A"}
Record *Record `json:"record"` Record *Record `json:"record"`
// example: 2 // example: 2
MissingSeriesEvalsToResolve *int `json:"missingSeriesEvalsToResolve,omitempty"` MissingSeriesEvalsToResolve *int64 `json:"missingSeriesEvalsToResolve,omitempty"`
} }
// swagger:route GET /v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RouteGetAlertRuleGroup // swagger:route GET /v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RouteGetAlertRuleGroup
@ -284,7 +284,7 @@ type AlertRuleExport struct {
IsPaused bool `json:"isPaused" yaml:"isPaused" hcl:"is_paused"` IsPaused bool `json:"isPaused" yaml:"isPaused" hcl:"is_paused"`
NotificationSettings *AlertRuleNotificationSettingsExport `json:"notification_settings,omitempty" yaml:"notification_settings,omitempty" hcl:"notification_settings,block"` NotificationSettings *AlertRuleNotificationSettingsExport `json:"notification_settings,omitempty" yaml:"notification_settings,omitempty" hcl:"notification_settings,block"`
Record *AlertRuleRecordExport `json:"record,omitempty" yaml:"record,omitempty" hcl:"record,block"` Record *AlertRuleRecordExport `json:"record,omitempty" yaml:"record,omitempty" hcl:"record,block"`
MissingSeriesEvalsToResolve *int `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty" hcl:"missing_series_evals_to_resolve"` MissingSeriesEvalsToResolve *int64 `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty" hcl:"missing_series_evals_to_resolve"`
} }
// AlertQueryExport is the provisioned export of models.AlertQuery. // AlertQueryExport is the provisioned export of models.AlertQuery.

View File

@ -299,10 +299,10 @@ func validateKeepFiringForInterval(ruleNode *apimodels.PostableExtendedRuleNode)
// - == 0, returns nil (reset to default) // - == 0, returns nil (reset to default)
// - == nil && UID == "", returns nil (new rule) // - == nil && UID == "", returns nil (new rule)
// - == nil && UID != "", returns -1 (existing rule) // - == nil && UID != "", returns -1 (existing rule)
func validateMissingSeriesEvalsToResolve(ruleNode *apimodels.PostableExtendedRuleNode) (*int, error) { func validateMissingSeriesEvalsToResolve(ruleNode *apimodels.PostableExtendedRuleNode) (*int64, error) {
if ruleNode.GrafanaManagedAlert.MissingSeriesEvalsToResolve == nil { if ruleNode.GrafanaManagedAlert.MissingSeriesEvalsToResolve == nil {
if ruleNode.GrafanaManagedAlert.UID != "" { if ruleNode.GrafanaManagedAlert.UID != "" {
return util.Pointer(-1), nil // will be patched later with the real value of the current version of the rule return util.Pointer[int64](-1), nil // will be patched later with the real value of the current version of the rule
} }
return nil, nil // if it's a new rule, use nil as the default return nil, nil // if it's a new rule, use nil as the default
} }

View File

@ -304,7 +304,7 @@ type AlertRule struct {
// required before resolving an alert state (a dimension) when data is missing. // required before resolving an alert state (a dimension) when data is missing.
// If nil, alerts resolve after 2 missing evaluation intervals // If nil, alerts resolve after 2 missing evaluation intervals
// (i.e., resolution occurs during the second evaluation where data is absent). // (i.e., resolution occurs during the second evaluation where data is absent).
MissingSeriesEvalsToResolve *int MissingSeriesEvalsToResolve *int64
} }
type AlertRuleMetadata struct { type AlertRuleMetadata struct {
@ -598,7 +598,7 @@ func (alertRule *AlertRule) GetGroupKey() AlertRuleGroupKey {
// to wait before resolving an alert rule instance when its data is missing. // to wait before resolving an alert rule instance when its data is missing.
// If not configured, it returns the default value (2), which means the alert // If not configured, it returns the default value (2), which means the alert
// resolves after missing for two evaluation intervals. // resolves after missing for two evaluation intervals.
func (alertRule *AlertRule) GetMissingSeriesEvalsToResolve() int { func (alertRule *AlertRule) GetMissingSeriesEvalsToResolve() int64 {
if alertRule.MissingSeriesEvalsToResolve == nil { if alertRule.MissingSeriesEvalsToResolve == nil {
return 2 // default value return 2 // default value
} }

View File

@ -554,8 +554,8 @@ func TestDiff(t *testing.T) {
if rule1.MissingSeriesEvalsToResolve != rule2.MissingSeriesEvalsToResolve { if rule1.MissingSeriesEvalsToResolve != rule2.MissingSeriesEvalsToResolve {
diff := diffs.GetDiffsForField("MissingSeriesEvalsToResolve") diff := diffs.GetDiffsForField("MissingSeriesEvalsToResolve")
assert.Len(t, diff, 1) assert.Len(t, diff, 1)
assert.Equal(t, *rule1.MissingSeriesEvalsToResolve, int(diff[0].Left.Int())) assert.Equal(t, *rule1.MissingSeriesEvalsToResolve, diff[0].Left.Int())
assert.Equal(t, *rule2.MissingSeriesEvalsToResolve, int(diff[0].Right.Int())) assert.Equal(t, *rule2.MissingSeriesEvalsToResolve, diff[0].Right.Int())
difCnt++ difCnt++
} }
@ -1001,14 +1001,14 @@ func TestAlertRuleGetMissingSeriesEvalsToResolve(t *testing.T) {
t.Run("should return the default 2 if MissingSeriesEvalsToResolve is nil", func(t *testing.T) { t.Run("should return the default 2 if MissingSeriesEvalsToResolve is nil", func(t *testing.T) {
rule := RuleGen.GenerateRef() rule := RuleGen.GenerateRef()
rule.MissingSeriesEvalsToResolve = nil rule.MissingSeriesEvalsToResolve = nil
require.Equal(t, 2, rule.GetMissingSeriesEvalsToResolve()) require.Equal(t, int64(2), rule.GetMissingSeriesEvalsToResolve())
}) })
t.Run("should return the correct value", func(t *testing.T) { t.Run("should return the correct value", func(t *testing.T) {
rule := RuleGen.With( rule := RuleGen.With(
RuleMuts.WithMissingSeriesEvalsToResolve(3), RuleMuts.WithMissingSeriesEvalsToResolve(3),
).GenerateRef() ).GenerateRef()
require.Equal(t, 3, rule.GetMissingSeriesEvalsToResolve()) require.Equal(t, int64(3), rule.GetMissingSeriesEvalsToResolve())
}) })
} }
@ -1113,7 +1113,7 @@ func TestValidateAlertRule(t *testing.T) {
t.Run("missingSeriesEvalsToResolve", func(t *testing.T) { t.Run("missingSeriesEvalsToResolve", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
missingSeriesEvalsToResolve *int missingSeriesEvalsToResolve *int64
expectedErrorContains string expectedErrorContains string
}{ }{
{ {
@ -1122,17 +1122,17 @@ func TestValidateAlertRule(t *testing.T) {
}, },
{ {
name: "should reject negative value", name: "should reject negative value",
missingSeriesEvalsToResolve: util.Pointer(-1), missingSeriesEvalsToResolve: util.Pointer[int64](-1),
expectedErrorContains: "field `missing_series_evals_to_resolve` must be greater than 0", expectedErrorContains: "field `missing_series_evals_to_resolve` must be greater than 0",
}, },
{ {
name: "should reject 0", name: "should reject 0",
missingSeriesEvalsToResolve: util.Pointer(0), missingSeriesEvalsToResolve: util.Pointer[int64](0),
expectedErrorContains: "field `missing_series_evals_to_resolve` must be greater than 0", expectedErrorContains: "field `missing_series_evals_to_resolve` must be greater than 0",
}, },
{ {
name: "should accept positive value", name: "should accept positive value",
missingSeriesEvalsToResolve: util.Pointer(2), missingSeriesEvalsToResolve: util.Pointer[int64](2),
}, },
} }

View File

@ -128,7 +128,7 @@ func (g *AlertRuleGenerator) Generate() AlertRule {
Labels: labels, Labels: labels,
NotificationSettings: ns, NotificationSettings: ns,
Metadata: GenerateMetadata(), Metadata: GenerateMetadata(),
MissingSeriesEvalsToResolve: util.Pointer(2), MissingSeriesEvalsToResolve: util.Pointer[int64](2),
} }
for _, mutator := range g.mutators { for _, mutator := range g.mutators {
@ -514,12 +514,12 @@ func (a *AlertRuleMutators) WithSameGroup() AlertRuleMutator {
} }
} }
func (a *AlertRuleMutators) WithMissingSeriesEvalsToResolve(timesOfInterval int) AlertRuleMutator { func (a *AlertRuleMutators) WithMissingSeriesEvalsToResolve(timesOfInterval int64) AlertRuleMutator {
return func(rule *AlertRule) { return func(rule *AlertRule) {
if timesOfInterval <= 0 { if timesOfInterval <= 0 {
panic("timesOfInterval must be greater than 0") panic("timesOfInterval must be greater than 0")
} }
rule.MissingSeriesEvalsToResolve = util.Pointer(timesOfInterval) rule.MissingSeriesEvalsToResolve = util.Pointer[int64](timesOfInterval)
} }
} }

View File

@ -275,7 +275,7 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom
// Prometheus resolves alerts as soon as the series disappears. // Prometheus resolves alerts as soon as the series disappears.
// By setting this value to 1 we ensure that the alert is resolved on the first evaluation // By setting this value to 1 we ensure that the alert is resolved on the first evaluation
// that doesn't have the series. // that doesn't have the series.
MissingSeriesEvalsToResolve: util.Pointer(1), MissingSeriesEvalsToResolve: util.Pointer[int64](1),
} }
if !isRecordingRule { if !isRecordingRule {

View File

@ -358,7 +358,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
require.Equal(t, models.Duration(evalOffset), grafanaRule.Data[0].RelativeTimeRange.To) require.Equal(t, models.Duration(evalOffset), grafanaRule.Data[0].RelativeTimeRange.To)
require.Equal(t, models.Duration(10*time.Minute+evalOffset), grafanaRule.Data[0].RelativeTimeRange.From) require.Equal(t, models.Duration(10*time.Minute+evalOffset), grafanaRule.Data[0].RelativeTimeRange.From)
require.Equal(t, util.Pointer(1), grafanaRule.MissingSeriesEvalsToResolve) require.Equal(t, util.Pointer(int64(1)), grafanaRule.MissingSeriesEvalsToResolve)
require.Equal(t, models.OkErrState, grafanaRule.ExecErrState) require.Equal(t, models.OkErrState, grafanaRule.ExecErrState)
require.Equal(t, models.OK, grafanaRule.NoDataState) require.Equal(t, models.OK, grafanaRule.NoDataState)

View File

@ -214,7 +214,7 @@ func TestRuleWithFolderFingerprint(t *testing.T) {
SimplifiedNotificationsSection: false, SimplifiedNotificationsSection: false,
}, },
}, },
MissingSeriesEvalsToResolve: util.Pointer(2), MissingSeriesEvalsToResolve: util.Pointer[int64](2),
} }
r2 := &models.AlertRule{ r2 := &models.AlertRule{
ID: 2, ID: 2,
@ -260,7 +260,7 @@ func TestRuleWithFolderFingerprint(t *testing.T) {
SimplifiedQueryAndExpressionsSection: true, SimplifiedQueryAndExpressionsSection: true,
}, },
}, },
MissingSeriesEvalsToResolve: util.Pointer(1), MissingSeriesEvalsToResolve: util.Pointer[int64](1),
} }
excludedFields := map[string]struct{}{ excludedFields := map[string]struct{}{

View File

@ -587,13 +587,13 @@ func (st *Manager) processMissingSeriesStates(logger log.Logger, evaluatedAt tim
// stateIsStale determines whether the evaluation state is considered stale. // stateIsStale determines whether the evaluation state is considered stale.
// A state is considered stale if the data has been missing for at least missingSeriesEvalsToResolve evaluation intervals. // A state is considered stale if the data has been missing for at least missingSeriesEvalsToResolve evaluation intervals.
func stateIsStale(evaluatedAt time.Time, lastEval time.Time, intervalSeconds int64, missingSeriesEvalsToResolve int) bool { func stateIsStale(evaluatedAt time.Time, lastEval time.Time, intervalSeconds int64, missingSeriesEvalsToResolve int64) bool {
// If the last evaluation time equals the current evaluation time, the state is not stale. // If the last evaluation time equals the current evaluation time, the state is not stale.
if evaluatedAt.Equal(lastEval) { if evaluatedAt.Equal(lastEval) {
return false return false
} }
resolveIfMissingDuration := time.Duration(int64(missingSeriesEvalsToResolve)*intervalSeconds) * time.Second resolveIfMissingDuration := time.Duration(missingSeriesEvalsToResolve*intervalSeconds) * time.Second
// timeSinceLastEval >= resolveIfMissingDuration // timeSinceLastEval >= resolveIfMissingDuration
return evaluatedAt.Sub(lastEval) >= resolveIfMissingDuration return evaluatedAt.Sub(lastEval) >= resolveIfMissingDuration

View File

@ -39,7 +39,7 @@ func TestStateIsStale(t *testing.T) {
name string name string
lastEvaluation time.Time lastEvaluation time.Time
expectedResult bool expectedResult bool
missingSeriesEvalsToResolve int missingSeriesEvalsToResolve int64
}{ }{
{ {
name: "false if last evaluation is now", name: "false if last evaluation is now",

View File

@ -30,7 +30,7 @@ type alertRule struct {
IsPaused bool IsPaused bool
NotificationSettings string `xorm:"notification_settings"` NotificationSettings string `xorm:"notification_settings"`
Metadata string `xorm:"metadata"` Metadata string `xorm:"metadata"`
MissingSeriesEvalsToResolve *int `xorm:"missing_series_evals_to_resolve"` MissingSeriesEvalsToResolve *int64 `xorm:"missing_series_evals_to_resolve"`
} }
func (a alertRule) TableName() string { func (a alertRule) TableName() string {
@ -68,7 +68,7 @@ type alertRuleVersion struct {
IsPaused bool IsPaused bool
NotificationSettings string `xorm:"notification_settings"` NotificationSettings string `xorm:"notification_settings"`
Metadata string `xorm:"metadata"` Metadata string `xorm:"metadata"`
MissingSeriesEvalsToResolve *int `xorm:"missing_series_evals_to_resolve"` MissingSeriesEvalsToResolve *int64 `xorm:"missing_series_evals_to_resolve"`
} }
// EqualSpec compares two alertRuleVersion objects for equality based on their specifications and returns true if they match. // EqualSpec compares two alertRuleVersion objects for equality based on their specifications and returns true if they match.

View File

@ -661,7 +661,7 @@ func TestIntegrationProvisioningRules(t *testing.T) {
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
}, },
}, },
MissingSeriesEvalsToResolve: util.Pointer(3), MissingSeriesEvalsToResolve: util.Pointer[int64](3),
}, },
}, },
} }
@ -676,7 +676,7 @@ func TestIntegrationProvisioningRules(t *testing.T) {
for _, rule := range result.Rules { for _, rule := range result.Rules {
require.NotEmpty(t, rule.UID) require.NotEmpty(t, rule.UID)
if rule.UID == "rule3" { if rule.UID == "rule3" {
require.Equal(t, 3, *rule.MissingSeriesEvalsToResolve) require.Equal(t, int64(3), *rule.MissingSeriesEvalsToResolve)
} }
} }
}) })

View File

@ -1761,42 +1761,42 @@ func TestIntegrationRuleUpdate(t *testing.T) {
t.Run("missing_series_evals_to_resolve", func(t *testing.T) { t.Run("missing_series_evals_to_resolve", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
initialValue *int initialValue *int64
updatedValue *int updatedValue *int64
expectedValue *int expectedValue *int64
expectedStatus int expectedStatus int
}{ }{
{ {
name: "should be able to set missing_series_evals_to_resolve to 5", name: "should be able to set missing_series_evals_to_resolve to 5",
initialValue: nil, initialValue: nil,
updatedValue: util.Pointer(5), updatedValue: util.Pointer[int64](5),
expectedValue: util.Pointer(5), expectedValue: util.Pointer[int64](5),
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
{ {
name: "should be able to update missing_series_evals_to_resolve", name: "should be able to update missing_series_evals_to_resolve",
initialValue: util.Pointer(1), initialValue: util.Pointer[int64](1),
updatedValue: util.Pointer(2), updatedValue: util.Pointer[int64](2),
expectedValue: util.Pointer(2), expectedValue: util.Pointer[int64](2),
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
{ {
name: "should preserve missing_series_evals_to_resolve when it's set nil", name: "should preserve missing_series_evals_to_resolve when it's set nil",
initialValue: util.Pointer(5), initialValue: util.Pointer[int64](5),
updatedValue: nil, updatedValue: nil,
expectedValue: util.Pointer(5), expectedValue: util.Pointer[int64](5),
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
{ {
name: "should reject missing_series_evals_to_resolve < 0", name: "should reject missing_series_evals_to_resolve < 0",
initialValue: util.Pointer(1), initialValue: util.Pointer[int64](1),
updatedValue: util.Pointer(-1), updatedValue: util.Pointer[int64](-1),
expectedStatus: http.StatusBadRequest, expectedStatus: http.StatusBadRequest,
}, },
{ {
name: "should be able to reset missing_series_evals_to_resolve by setting it to 0", name: "should be able to reset missing_series_evals_to_resolve by setting it to 0",
initialValue: util.Pointer(1), initialValue: util.Pointer[int64](1),
updatedValue: util.Pointer(0), updatedValue: util.Pointer[int64](0),
expectedValue: nil, expectedValue: nil,
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
@ -3474,7 +3474,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
}, },
NoDataState: apimodels.NoDataState(ngmodels.Alerting), NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState), ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
MissingSeriesEvalsToResolve: util.Pointer(2), // If UID is specified, this field is required MissingSeriesEvalsToResolve: util.Pointer[int64](2), // If UID is specified, this field is required
}, },
}, },
}, },