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:
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user