Alerting: Add recording rule target datasource support to Prometheus conversion API (#101799)
What is this feature? Adds target datasource UID to the recording rules so that they write to the same datasource used for alerting rule queries after the import. Why do we need this feature? Target datasourse support was added in #101678, and under a feature flag grafanaManagedRecordingRulesDatasources (#101778). This PR makes the importing process: Check if the import contains recording rules Verify both recording rules and the grafanaManagedRecordingRulesDatasources feature flag are enabled If either check fails, return an error If both checks pass, create recording rules with the provided datasource UID set as both the query and target datasource
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
dsfakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
|
||||
@@ -48,6 +49,13 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
{
|
||||
Record: "recorded-metric",
|
||||
Expr: "vector(1)",
|
||||
Labels: map[string]string{
|
||||
"severity": "warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -104,21 +112,39 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
|
||||
// Get the updated rule
|
||||
// Get the rules
|
||||
remaining, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
||||
OrgID: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, remaining, 1)
|
||||
require.Len(t, remaining, 2)
|
||||
|
||||
require.Equal(t, simpleGroup.Name, remaining[0].RuleGroup)
|
||||
require.Equal(t, fmt.Sprintf("[%s] %s", simpleGroup.Name, simpleGroup.Rules[0].Alert), remaining[0].Title)
|
||||
promRuleYAML, err := yaml.Marshal(simpleGroup.Rules[0])
|
||||
require.NoError(t, err)
|
||||
// Create a map of rule titles to their expected definitions
|
||||
expectedRules := make(map[string]string)
|
||||
for _, rule := range simpleGroup.Rules {
|
||||
if rule.Alert != "" {
|
||||
title := fmt.Sprintf("[%s] %s", simpleGroup.Name, rule.Alert)
|
||||
promRuleYAML, err := yaml.Marshal(rule)
|
||||
require.NoError(t, err)
|
||||
expectedRules[title] = string(promRuleYAML)
|
||||
} else if rule.Record != "" {
|
||||
title := fmt.Sprintf("[%s] %s", simpleGroup.Name, rule.Record)
|
||||
promRuleYAML, err := yaml.Marshal(rule)
|
||||
require.NoError(t, err)
|
||||
expectedRules[title] = string(promRuleYAML)
|
||||
}
|
||||
}
|
||||
|
||||
promDefinition, err := remaining[0].PrometheusRuleDefinition()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(promRuleYAML), promDefinition)
|
||||
// Verify each rule matches its expected definition
|
||||
for _, r := range remaining {
|
||||
require.Equal(t, simpleGroup.Name, r.RuleGroup)
|
||||
expectedDef, exists := expectedRules[r.Title]
|
||||
require.True(t, exists, "unexpected rule title: %s", r.Title)
|
||||
|
||||
promDefinition, err := r.PrometheusRuleDefinition()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedDef, promDefinition)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail to replace a provisioned rule group", func(t *testing.T) {
|
||||
@@ -263,6 +289,58 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
})
|
||||
|
||||
t.Run("with disabled recording rules", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
recordingRules bool
|
||||
recordingRulesTargetDS bool
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "when recording rules are enabled",
|
||||
recordingRules: true,
|
||||
recordingRulesTargetDS: true,
|
||||
expectedStatus: http.StatusAccepted,
|
||||
},
|
||||
{
|
||||
name: "when recording rules are disabled",
|
||||
recordingRules: false,
|
||||
recordingRulesTargetDS: true,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "when target datasources for recording rules are disabled",
|
||||
recordingRules: true,
|
||||
recordingRulesTargetDS: false,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "when both recording rules and target datasources are disabled",
|
||||
recordingRules: false,
|
||||
recordingRulesTargetDS: false,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var features featuremgmt.FeatureToggles
|
||||
if tc.recordingRulesTargetDS {
|
||||
features = featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRulesDatasources)
|
||||
} else {
|
||||
features = featuremgmt.WithFeatures()
|
||||
}
|
||||
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t, withFeatureToggles(features))
|
||||
srv.cfg.RecordingRules.Enabled = tc.recordingRules
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, tc.expectedStatus, response.Status())
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
|
||||
@@ -783,6 +861,7 @@ type convertPrometheusSrvOptions struct {
|
||||
provenanceStore provisioning.ProvisioningStore
|
||||
fakeAccessControlRuleService *acfakes.FakeRuleService
|
||||
quotaChecker *provisioning.MockQuotaChecker
|
||||
featureToggles featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions)
|
||||
@@ -805,6 +884,12 @@ func withQuotaChecker(checker *provisioning.MockQuotaChecker) convertPrometheusS
|
||||
}
|
||||
}
|
||||
|
||||
func withFeatureToggles(toggles featuremgmt.FeatureToggles) convertPrometheusSrvOptionsFunc {
|
||||
return func(opts *convertPrometheusSrvOptions) {
|
||||
opts.featureToggles = toggles
|
||||
}
|
||||
}
|
||||
|
||||
func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) {
|
||||
t.Helper()
|
||||
|
||||
@@ -816,6 +901,7 @@ func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOption
|
||||
provenanceStore: fakes.NewFakeProvisioningStore(),
|
||||
fakeAccessControlRuleService: &acfakes.FakeRuleService{},
|
||||
quotaChecker: quotas,
|
||||
featureToggles: featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRulesDatasources),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
@@ -851,9 +937,12 @@ func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOption
|
||||
|
||||
cfg := &setting.UnifiedAlertingSettings{
|
||||
DefaultRuleEvaluationInterval: 1 * time.Minute,
|
||||
RecordingRules: setting.RecordingRuleSettings{
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService)
|
||||
srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService, options.featureToggles)
|
||||
|
||||
return srv, dsCache, ruleStore, folderService
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user