diff --git a/pkg/services/ngalert/api/api_ruler_validation_test.go b/pkg/services/ngalert/api/api_ruler_validation_test.go index 98fbb20cf12..553a02cff30 100644 --- a/pkg/services/ngalert/api/api_ruler_validation_test.go +++ b/pkg/services/ngalert/api/api_ruler_validation_test.go @@ -493,6 +493,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) { r.GrafanaManagedAlert.NoDataState = apimodels.OK r.GrafanaManagedAlert.ExecErrState = apimodels.AlertingErrState r.GrafanaManagedAlert.NotificationSettings = &apimodels.AlertRuleNotificationSettings{} + r.GrafanaManagedAlert.MissingSeriesEvalsToResolve = util.Pointer[int64](1) r.For = func() *model.Duration { five := model.Duration(time.Second * 5); return &five }() r.KeepFiringFor = func() *model.Duration { five := model.Duration(time.Second * 5); return &five }() return &r @@ -502,6 +503,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) { require.Empty(t, alert.NoDataState) require.Empty(t, alert.ExecErrState) require.Nil(t, alert.NotificationSettings) + require.Nil(t, alert.MissingSeriesEvalsToResolve) require.Zero(t, alert.For) require.Zero(t, alert.KeepFiringFor) }, diff --git a/pkg/services/ngalert/api/compat/compat.go b/pkg/services/ngalert/api/compat/compat.go index 5fda13672ba..37c9a8db6ed 100644 --- a/pkg/services/ngalert/api/compat/compat.go +++ b/pkg/services/ngalert/api/compat/compat.go @@ -189,42 +189,11 @@ func AlertRuleExportFromAlertRule(rule models.AlertRule) (definitions.AlertRuleE data = append(data, query) } - cPtr := &rule.Condition - if rule.Condition == "" { - cPtr = nil - } - - noDataState := definitions.NoDataState(rule.NoDataState) - ndsPtr := &noDataState - if noDataState == "" { - ndsPtr = nil - } - execErrorState := definitions.ExecutionErrorState(rule.ExecErrState) - eesPtr := &execErrorState - if execErrorState == "" { - eesPtr = nil - } - result := definitions.AlertRuleExport{ - UID: rule.UID, - Title: rule.Title, - For: model.Duration(rule.For), - KeepFiringFor: model.Duration(rule.KeepFiringFor), - Condition: cPtr, - Data: data, - DashboardUID: rule.DashboardUID, - PanelID: rule.PanelID, - NoDataState: ndsPtr, - ExecErrState: eesPtr, - IsPaused: rule.IsPaused, - NotificationSettings: AlertRuleNotificationSettingsExportFromNotificationSettings(rule.NotificationSettings), - Record: AlertRuleRecordExportFromRecord(rule.Record), - } - if rule.For.Seconds() > 0 { - result.ForString = util.Pointer(model.Duration(rule.For).String()) - } - if rule.KeepFiringFor.Seconds() > 0 { - result.KeepFiringForString = util.Pointer(model.Duration(rule.KeepFiringFor).String()) + UID: rule.UID, + Title: rule.Title, + Data: data, + IsPaused: rule.IsPaused, } if rule.Annotations != nil { result.Annotations = &rule.Annotations @@ -232,13 +201,54 @@ func AlertRuleExportFromAlertRule(rule models.AlertRule) (definitions.AlertRuleE if rule.Labels != nil { result.Labels = &rule.Labels } - if rule.MissingSeriesEvalsToResolve != nil && *rule.MissingSeriesEvalsToResolve != -1 { - result.MissingSeriesEvalsToResolve = rule.MissingSeriesEvalsToResolve + + if rule.Type() == models.RuleTypeRecording { + populateRecordingRuleExportFields(rule, &result) + } else { + populateAlertingRuleExportFields(rule, &result) } return result, nil } +func populateRecordingRuleExportFields(rule models.AlertRule, result *definitions.AlertRuleExport) { + result.Record = AlertRuleRecordExportFromRecord(rule.Record) +} + +func populateAlertingRuleExportFields(rule models.AlertRule, result *definitions.AlertRuleExport) { + result.DashboardUID = rule.DashboardUID + result.PanelID = rule.PanelID + result.NotificationSettings = AlertRuleNotificationSettingsExportFromNotificationSettings(rule.NotificationSettings) + + if rule.Condition != "" { + result.Condition = &rule.Condition + } + + if rule.NoDataState != "" { + noDataState := definitions.NoDataState(rule.NoDataState) + result.NoDataState = &noDataState + } + + if rule.ExecErrState != "" { + execErrorState := definitions.ExecutionErrorState(rule.ExecErrState) + result.ExecErrState = &execErrorState + } + + result.For = model.Duration(rule.For) + if rule.For > 0 { + result.ForString = util.Pointer(model.Duration(rule.For).String()) + } + + result.KeepFiringFor = model.Duration(rule.KeepFiringFor) + if rule.KeepFiringFor > 0 { + result.KeepFiringForString = util.Pointer(model.Duration(rule.KeepFiringFor).String()) + } + + if rule.MissingSeriesEvalsToResolve != nil && *rule.MissingSeriesEvalsToResolve != -1 { + result.MissingSeriesEvalsToResolve = rule.MissingSeriesEvalsToResolve + } +} + func encodeQueryModel(m map[string]any) (string, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) diff --git a/pkg/services/ngalert/api/compat/compat_test.go b/pkg/services/ngalert/api/compat/compat_test.go index 4a107335945..8b50c609559 100644 --- a/pkg/services/ngalert/api/compat/compat_test.go +++ b/pkg/services/ngalert/api/compat/compat_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/util" ) func TestToModel(t *testing.T) { @@ -115,6 +116,102 @@ func TestToModel(t *testing.T) { }) } +func TestAlertRuleExportFromAlertRule(t *testing.T) { + alertingRule := models.RuleGen.With( + models.RuleGen.WithNotEmptyLabels(2, "lbl-"), + models.RuleGen.WithAnnotations(map[string]string{"ann-key": "ann-value"}), + models.RuleGen.WithFor(2*time.Minute), + models.RuleGen.WithKeepFiringFor(5*time.Minute), + models.RuleGen.WithNotificationSettingsGen(models.NotificationSettingsGen()), + ).Generate() + recordingRule := models.RuleGen.With( + models.RuleGen.WithAllRecordingRules(), + models.RuleGen.WithNotEmptyLabels(2, "lbl-"), + models.RuleGen.WithAnnotations(map[string]string{"ann-key": "ann-value"}), + ).Generate() + + // Build expected exported recording rule + recordingRuleData, err := AlertQueryExportFromAlertQuery(recordingRule.Data[0]) + require.NoError(t, err) + expectedRecordingRuleExport := definitions.AlertRuleExport{ + UID: recordingRule.UID, + Title: recordingRule.Title, + Data: []definitions.AlertQueryExport{recordingRuleData}, + Annotations: &recordingRule.Annotations, + Labels: &recordingRule.Labels, + Record: &definitions.AlertRuleRecordExport{ + Metric: recordingRule.Record.Metric, + From: recordingRule.Record.From, + TargetDatasourceUID: util.Pointer(recordingRule.Record.TargetDatasourceUID), + }, + } + + // Build expected exported alerting rule + alertingRuleData, err := AlertQueryExportFromAlertQuery(alertingRule.Data[0]) + require.NoError(t, err) + noDataState := definitions.NoDataState(alertingRule.NoDataState) + execErrState := definitions.ExecutionErrorState(alertingRule.ExecErrState) + expectedAlertingRuleExport := definitions.AlertRuleExport{ + UID: alertingRule.UID, + Title: alertingRule.Title, + Condition: &alertingRule.Condition, + Data: []definitions.AlertQueryExport{alertingRuleData}, + DashboardUID: alertingRule.DashboardUID, + PanelID: alertingRule.PanelID, + NoDataState: &noDataState, + ExecErrState: &execErrState, + For: prommodel.Duration(alertingRule.For), + KeepFiringFor: prommodel.Duration(alertingRule.KeepFiringFor), + ForString: util.Pointer(prommodel.Duration(alertingRule.For).String()), + KeepFiringForString: util.Pointer(prommodel.Duration(alertingRule.KeepFiringFor).String()), + Annotations: &alertingRule.Annotations, + Labels: &alertingRule.Labels, + NotificationSettings: AlertRuleNotificationSettingsExportFromNotificationSettings(alertingRule.NotificationSettings), + MissingSeriesEvalsToResolve: alertingRule.MissingSeriesEvalsToResolve, + } + + testCases := []struct { + name string + rule models.AlertRule + expected definitions.AlertRuleExport + }{ + { + name: "export recording rule", + rule: recordingRule, + expected: expectedRecordingRuleExport, + }, + { + name: "export alerting rule", + rule: alertingRule, + expected: expectedAlertingRuleExport, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + exported, err := AlertRuleExportFromAlertRule(tc.rule) + require.NoError(t, err) + require.Equal(t, tc.expected, exported) + }) + } +} + +func TestAlertQueryExportFromAlertQuery(t *testing.T) { + query := models.RuleGen.GenerateQuery() + + exported, err := AlertQueryExportFromAlertQuery(query) + require.NoError(t, err) + + require.Equal(t, query.RefID, exported.RefID) + require.Equal(t, query.DatasourceUID, exported.DatasourceUID) + require.Equal(t, int64(time.Duration(query.RelativeTimeRange.From).Seconds()), exported.RelativeTimeRange.FromSeconds) + require.Equal(t, int64(time.Duration(query.RelativeTimeRange.To).Seconds()), exported.RelativeTimeRange.ToSeconds) + require.NotNil(t, exported.QueryType) + require.Equal(t, query.QueryType, *exported.QueryType) + require.NotNil(t, exported.Model) + require.NotEmpty(t, exported.ModelString) +} + func TestAlertRuleMetadataFromModelMetadata(t *testing.T) { t.Run("should convert model metadata to api metadata", func(t *testing.T) { modelMetadata := models.AlertRuleMetadata{ diff --git a/pkg/services/ngalert/api/validation/api_ruler_validation.go b/pkg/services/ngalert/api/validation/api_ruler_validation.go index c2baf8108ba..5a74c58f90e 100644 --- a/pkg/services/ngalert/api/validation/api_ruler_validation.go +++ b/pkg/services/ngalert/api/validation/api_ruler_validation.go @@ -193,6 +193,7 @@ func validateRecordingRuleFields(in *apimodels.PostableExtendedRuleNode, newRule newRule.For = 0 newRule.KeepFiringFor = 0 newRule.NotificationSettings = nil + newRule.MissingSeriesEvalsToResolve = nil return newRule, nil } diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index 13c95e70aa4..0e8ebf647d4 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -272,16 +272,16 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom RuleGroup: promGroup.Name, IsPaused: isPaused, Record: record, + } + + if !isRecordingRule { + result.NotificationSettings = p.cfg.NotificationSettings // MissingSeriesEvalsToResolve is set to 1 to match the Prometheus behaviour. // 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 // that doesn't have the series. - MissingSeriesEvalsToResolve: util.Pointer[int64](1), - } - - if !isRecordingRule { - result.NotificationSettings = p.cfg.NotificationSettings + result.MissingSeriesEvalsToResolve = util.Pointer[int64](1) } if p.cfg.KeepOriginalRuleDefinition != nil && *p.cfg.KeepOriginalRuleDefinition { diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 503cd76dd64..d9542e24c5e 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -358,7 +358,12 @@ func TestPrometheusRulesToGrafana(t *testing.T) { 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, util.Pointer(int64(1)), grafanaRule.MissingSeriesEvalsToResolve) + + if promRule.Record != "" { + require.Nil(t, grafanaRule.MissingSeriesEvalsToResolve) + } else { + require.Equal(t, util.Pointer(int64(1)), grafanaRule.MissingSeriesEvalsToResolve) + } require.Equal(t, models.OkErrState, grafanaRule.ExecErrState) require.Equal(t, models.OK, grafanaRule.NoDataState)