Alerting: Remove grafanaManagedRecordingRules feature flag (#105569)

This commit is contained in:
Alexander Akhmetov
2025-05-19 12:15:49 +02:00
committed by GitHub
parent f83fb35bfb
commit c58ac15031
16 changed files with 47 additions and 173 deletions
@@ -63,9 +63,7 @@ To configure Grafana-managed recording rules, complete the following steps.
This section only applies to Grafana OSS and Grafana Enterprise.
First, enable the `grafanaManagedRecordingRules` [feature flag](ref:configure-feature-toggles).
Then, enable the feature by setting `enabled = true` in the `[recording_rules]` section of the Grafana config .ini. Provide the URL of your Prometheus-compatible remote-write endpoint in the `url` field, along with optional credentials or headers.
Enable the feature by setting `enabled = true` in the `[recording_rules]` section of the Grafana configuration file. Provide the URL of your Prometheus-compatible remote-write endpoint in the `url` field, along with optional credentials or headers.
```
[recording_rules]
@@ -558,10 +558,6 @@ export interface FeatureToggles {
*/
disableNumericMetricsSortingInExpressions?: boolean;
/**
* Enables Grafana-managed recording rules.
*/
grafanaManagedRecordingRules?: boolean;
/**
* Enables Query Library feature in Explore
*/
queryLibrary?: boolean;
-9
View File
@@ -951,15 +951,6 @@ var (
Owner: grafanaOSSBigTent,
RequiresRestart: true,
},
{
Name: "grafanaManagedRecordingRules",
Description: "Enables Grafana-managed recording rules.",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
AllowSelfServe: false,
HideFromDocs: true,
HideFromAdminPage: true,
},
{
Name: "queryLibrary",
Description: "Enables Query Library feature in Explore",
-1
View File
@@ -123,7 +123,6 @@ oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,fals
newDashboardWithFiltersAndGroupBy,experimental,@grafana/dashboards-squad,false,false,false
cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false
disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false
grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
queryLibrary,experimental,@grafana/grafana-frontend-platform,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
123 newDashboardWithFiltersAndGroupBy experimental @grafana/dashboards-squad false false false
124 cloudWatchNewLabelParsing GA @grafana/aws-datasources false false false
125 disableNumericMetricsSortingInExpressions experimental @grafana/oss-big-tent false true false
grafanaManagedRecordingRules experimental @grafana/alerting-squad false false false
126 queryLibrary experimental @grafana/grafana-frontend-platform false false false
127 logsExploreTableDefaultVisualization experimental @grafana/observability-logs false false true
128 newDashboardSharingComponent GA @grafana/sharing-squad false false true
-4
View File
@@ -503,10 +503,6 @@ const (
// In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels.
FlagDisableNumericMetricsSortingInExpressions = "disableNumericMetricsSortingInExpressions"
// FlagGrafanaManagedRecordingRules
// Enables Grafana-managed recording rules.
FlagGrafanaManagedRecordingRules = "grafanaManagedRecordingRules"
// FlagQueryLibrary
// Enables Query Library feature in Explore
FlagQueryLibrary = "queryLibrary"
+2 -1
View File
@@ -1401,7 +1401,8 @@
"metadata": {
"name": "grafanaManagedRecordingRules",
"resourceVersion": "1743693517832",
"creationTimestamp": "2024-04-22T17:53:16Z"
"creationTimestamp": "2024-04-22T17:53:16Z",
"deletionTimestamp": "2025-05-16T18:56:20Z"
},
"spec": {
"description": "Enables Grafana-managed recording rules.",
@@ -344,14 +344,6 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar de
return ErrResp(http.StatusBadRequest, err, "")
}
if upstreamModel.Type() == alerting_models.RuleTypeRecording && !srv.featureManager.IsEnabledGlobally(featuremgmt.FlagGrafanaManagedRecordingRules) {
return ErrResp(
http.StatusBadRequest,
fmt.Errorf("%w: recording rules cannot be created on this instance", alerting_models.ErrAlertRuleFailedValidation),
"",
)
}
provenance := determineProvenance(c)
createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), c.SignedInUser, upstreamModel, alerting_models.Provenance(provenance))
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
@@ -377,14 +369,6 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar def
ErrResp(http.StatusBadRequest, err, "")
}
if updated.Type() == alerting_models.RuleTypeRecording && !srv.featureManager.IsEnabledGlobally(featuremgmt.FlagGrafanaManagedRecordingRules) {
return ErrResp(
http.StatusBadRequest,
fmt.Errorf("%w: recording rules cannot be created on this instance", alerting_models.ErrAlertRuleFailedValidation),
"",
)
}
if UID == "" {
// If there is no UID, return 404 as the UID is part of the URL.
return response.Empty(http.StatusNotFound)
@@ -534,95 +534,46 @@ func TestProvisioningApi(t *testing.T) {
})
t.Run("recording rules", func(t *testing.T) {
t.Run("are enabled", func(t *testing.T) {
env := createTestEnv(t, testConfig)
env.features = featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRules)
env := createTestEnv(t, testConfig)
t.Run("POST returns 201", func(t *testing.T) {
sut := createProvisioningSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rule := createTestRecordingRule("rule", 1)
t.Run("POST returns 201", func(t *testing.T) {
sut := createProvisioningSrvSutFromEnv(t, &env)
rc := createTestRequestCtx()
rule := createTestRecordingRule("rule", 1)
response := sut.RoutePostAlertRule(&rc, rule)
response := sut.RoutePostAlertRule(&rc, rule)
require.Equal(t, 201, response.Status())
})
t.Run("PUT returns 200", func(t *testing.T) {
sut := createProvisioningSrvSutFromEnv(t, &env)
uid := util.GenerateShortUID()
rule := createTestAlertRule("rule", 3)
rule.UID = uid
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: rule.FolderUID,
Title: "Folder Title",
OrgID: rule.OrgID,
SignedInUser: &user.SignedInUser{OrgID: rule.OrgID},
})
require.NoError(t, err)
insertRuleInOrg(t, sut, rule, 3)
// make rule a recording rule
rule.Record = &definitions.Record{
Metric: "test_metric",
From: "A",
}
rc := createTestRequestCtx()
rc.OrgID = 3
response := sut.RoutePutAlertRule(&rc, rule, rule.UID)
require.Equal(t, 200, response.Status())
})
require.Equal(t, 201, response.Status())
})
t.Run("are not enabled", func(t *testing.T) {
t.Run("POST returns 400", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
rc := createTestRequestCtx()
rule := createTestRecordingRule("rule", 1)
t.Run("PUT returns 200", func(t *testing.T) {
sut := createProvisioningSrvSutFromEnv(t, &env)
uid := util.GenerateShortUID()
rule := createTestAlertRule("rule", 3)
rule.UID = uid
response := sut.RoutePostAlertRule(&rc, rule)
require.Equal(t, 400, response.Status())
require.NotEmpty(t, response.Body())
require.Contains(t, string(response.Body()), "recording rules cannot be created on this instance")
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: rule.FolderUID,
Title: "Folder Title",
OrgID: rule.OrgID,
SignedInUser: &user.SignedInUser{OrgID: rule.OrgID},
})
require.NoError(t, err)
t.Run("PUT returns 400", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
uid := util.GenerateShortUID()
rule := createTestAlertRule("rule", 3)
rule.UID = uid
insertRuleInOrg(t, sut, rule, 3)
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: rule.FolderUID,
Title: "Folder Title",
OrgID: rule.OrgID,
SignedInUser: &user.SignedInUser{OrgID: rule.OrgID},
})
require.NoError(t, err)
// make rule a recording rule
rule.Record = &definitions.Record{
Metric: "test_metric",
From: "A",
}
insertRuleInOrg(t, sut, rule, 3)
rc := createTestRequestCtx()
rc.OrgID = 3
// make rule a recording rule
rule.Record = &definitions.Record{
Metric: "test_metric",
From: "A",
}
response := sut.RoutePutAlertRule(&rc, rule, rule.UID)
rc := createTestRequestCtx()
rc.OrgID = 3
response := sut.RoutePutAlertRule(&rc, rule, rule.UID)
require.Equal(t, 400, response.Status())
require.NotEmpty(t, response.Body())
require.Contains(t, string(response.Body()), "recording rules cannot be created on this instance")
})
require.Equal(t, 200, response.Status())
})
})
@@ -974,7 +974,6 @@ func createService(store *fakes.RuleStore, _userService *usertest.FakeUserServic
authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())),
amConfigStore: &fakeAMRefresher{},
amRefresher: &fakeAMRefresher{},
featureManager: featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRules),
userService: userService,
conditionValidator: &recordingConditionValidator{},
}
@@ -51,11 +51,6 @@ func makeLimits(cfg *setting.UnifiedAlertingSettings) RuleLimits {
return RuleLimitsFromConfig(cfg, baseToggles)
}
func allowRecording(lim RuleLimits) *RuleLimits {
lim.RecordingRulesAllowed = true
return &lim
}
func validRule() apimodels.PostableExtendedRuleNode {
forDuration := model.Duration(rand.Int64N(1000))
keepFiringForDuration := model.Duration(rand.Int64N(1000))
@@ -447,8 +442,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) {
},
},
{
name: "accepts and converts recording rule when toggle is enabled",
limits: allowRecording(limits),
name: "accepts and converts recording rule",
rule: func() *apimodels.PostableExtendedRuleNode {
r := validRule()
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"}
@@ -489,8 +483,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) {
},
},
{
name: "recording rules ignore fields that only make sense for Alerting rules",
limits: allowRecording(limits),
name: "recording rules ignore fields that only make sense for Alerting rules",
rule: func() *apimodels.PostableExtendedRuleNode {
r := validRule()
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"}
@@ -673,22 +666,7 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
},
},
{
name: "rejects valid recording rules if toggle is disabled",
rule: func() *apimodels.PostableExtendedRuleNode {
r := validRule()
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"}
r.GrafanaManagedAlert.Condition = ""
r.GrafanaManagedAlert.NoDataState = ""
r.GrafanaManagedAlert.ExecErrState = ""
r.GrafanaManagedAlert.NotificationSettings = nil
r.For = nil
return &r
},
expErr: "recording rules cannot be created",
},
{
name: "rejects recording rule with invalid metric name",
limits: allowRecording(limits),
name: "rejects recording rule with invalid metric name",
rule: func() *apimodels.PostableExtendedRuleNode {
r := validRule()
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "", From: "A"}
@@ -702,8 +680,7 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
expErr: "must be a valid Prometheus metric name",
},
{
name: "rejects recording rule with empty from",
limits: allowRecording(limits),
name: "rejects recording rule with empty from",
rule: func() *apimodels.PostableExtendedRuleNode {
r := validRule()
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "my_metric", From: ""}
@@ -717,8 +694,7 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
expErr: "cannot be empty",
},
{
name: "rejects recording rule with from not matching",
limits: allowRecording(limits),
name: "rejects recording rule with from not matching",
rule: func() *apimodels.PostableExtendedRuleNode {
r := validRule()
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "my_metric", From: "NOTEXIST"}
@@ -23,15 +23,12 @@ type RuleLimits struct {
DefaultRuleEvaluationInterval time.Duration
// All intervals must be an integer multiple of this duration.
BaseInterval time.Duration
// Whether recording rules are allowed.
RecordingRulesAllowed bool
}
func RuleLimitsFromConfig(cfg *setting.UnifiedAlertingSettings, toggles featuremgmt.FeatureToggles) RuleLimits {
return RuleLimits{
DefaultRuleEvaluationInterval: cfg.DefaultRuleEvaluationInterval,
BaseInterval: cfg.BaseInterval,
RecordingRulesAllowed: toggles.IsEnabledGlobally(featuremgmt.FlagGrafanaManagedRecordingRules),
}
}
@@ -176,10 +173,6 @@ func validateAlertingRuleFields(in *apimodels.PostableExtendedRuleNode, newRule
// validateRecordingRuleFields validates only the fields on a rule that are specific to Recording rules.
// it will load fields that pass validation onto newRule and return the result.
func validateRecordingRuleFields(in *apimodels.PostableExtendedRuleNode, newRule ngmodels.AlertRule, limits RuleLimits, canPatch bool) (ngmodels.AlertRule, error) {
if !limits.RecordingRulesAllowed {
return ngmodels.AlertRule{}, fmt.Errorf("%w: recording rules cannot be created on this instance", ngmodels.ErrAlertRuleFailedValidation)
}
err := ValidateCondition(in.GrafanaManagedAlert.Record.From, in.GrafanaManagedAlert.Data, canPatch)
if err != nil {
return ngmodels.AlertRule{}, fmt.Errorf("%w: %s", ngmodels.ErrAlertRuleFailedValidation, err.Error())
-4
View File
@@ -317,10 +317,6 @@ func (ng *AlertNG) init() error {
evalFactory := eval.NewEvaluatorFactory(ng.Cfg.UnifiedAlerting, ng.DataSourceCache, ng.ExpressionService)
conditionValidator := eval.NewConditionValidator(ng.DataSourceCache, ng.ExpressionService, ng.pluginsStore)
if !ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagGrafanaManagedRecordingRules) {
// Force-disable the feature if the feature toggle is not on - sets us up for feature toggle removal.
ng.Cfg.UnifiedAlerting.RecordingRules.Enabled = false
}
recordingWriter, err := createRecordingWriter(ng.FeatureToggles, ng.Cfg.UnifiedAlerting.RecordingRules, ng.httpClientProvider, ng.DataSourceService, clk, ng.Metrics.GetRemoteWriterMetrics())
if err != nil {
return fmt.Errorf("failed to initialize recording writer: %w", err)
@@ -30,7 +30,7 @@ func TestIntegrationConvertPrometheusNotificationSettings(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -113,7 +113,7 @@ func TestIntegrationConvertPrometheusEndpoints_RecordingRuleTargetDatasource(t *
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -186,7 +186,7 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -398,7 +398,7 @@ func TestIntegrationConvertPrometheusEndpoints_UpdateRule(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -485,7 +485,7 @@ func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -573,7 +573,7 @@ func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -687,7 +687,7 @@ func TestIntegrationConvertPrometheusEndpoints_FolderUIDHeader(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -789,7 +789,7 @@ func TestIntegrationConvertPrometheusEndpoints_Provenance(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -905,7 +905,7 @@ func TestIntegrationConvertPrometheusEndpoints_Delete(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources"},
EnableRecordingRules: true,
})
@@ -58,10 +58,7 @@ export const GrafanaRules = ({ namespaces, expandAll }: Props) => {
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
const hasGrafanaAlerts = namespaces.length > 0;
const { canCreateGrafanaRules } = useRulesAccess();
const grafanaRecordingRulesEnabled =
config.unifiedAlerting.recordingRulesEnabled &&
config.featureToggles.grafanaManagedRecordingRules &&
canCreateGrafanaRules;
const grafanaRecordingRulesEnabled = config.unifiedAlerting.recordingRulesEnabled && canCreateGrafanaRules;
const { t } = useTranslate();
return (
<section className={styles.wrapper}>
@@ -7,10 +7,7 @@ import { useRulesAccess } from '../../utils/accessControlHooks';
const RecordingRulesButtons = () => {
const { canCreateGrafanaRules, canCreateCloudRules } = useRulesAccess();
const grafanaRecordingRulesEnabled =
config.unifiedAlerting.recordingRulesEnabled &&
config.featureToggles.grafanaManagedRecordingRules &&
canCreateGrafanaRules;
const grafanaRecordingRulesEnabled = config.unifiedAlerting.recordingRulesEnabled && canCreateGrafanaRules;
const { t } = useTranslate();
const canCreateAll = canCreateGrafanaRules && canCreateCloudRules && grafanaRecordingRulesEnabled;