Alerting: Fix export of imported Prometheus-style recording rules to terraform (#115661)

Alerting: Fix export imported Prometheus-style recording rules to terraform
This commit is contained in:
Alexander Akhmetov
2025-12-24 09:49:38 +01:00
committed by GitHub
parent 4f57ebe4ad
commit c38e515dec
6 changed files with 158 additions and 43 deletions
@@ -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)
},
+47 -37
View File
@@ -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)
@@ -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{
@@ -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
}
+5 -5
View File
@@ -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 {
+6 -1
View File
@@ -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)