Alerting: Allow disabling recording rules write for a data source in the UI (#106664)

Adds a new "Allow as recording rules target" toggle to Prometheus datasource configuration that controls whether the datasource can be selected as a target for writing recording rules.
---------

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
This commit is contained in:
Alexander Akhmetov
2025-06-17 22:55:45 +02:00
committed by GitHub
co-authored by ismail simsek Konrad Lalik
parent 6cb2c701e6
commit f6e330b3d7
19 changed files with 201 additions and 20 deletions
+2
View File
@@ -4406,6 +4406,8 @@ exports[`no gf-form usage`] = {
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
+4
View File
@@ -489,6 +489,10 @@ concurrent_query_count = 10
# It only works if the data source's `jsonData.manageAlerts` prop does not contain a previously configured value.
default_manage_alerts_ui_toggle = true
# Default behavior for the "Allow as recording rules target" toggle when configuring a data source.
# It only works if the data source's `jsonData.allowAsRecordingRulesTarget` prop does not contain a previously configured value.
default_allow_recording_rules_target_alerts_ui_toggle = true
################################### SQL Data Sources #####################
[sql_datasources]
# Default maximum number of open connections maintained in the connection pool
+4
View File
@@ -487,6 +487,10 @@
# It only works if the data source's `jsonData.manageAlerts` prop does not contain a previously configured value.
;default_manage_alerts_ui_toggle = true
# Default behavior for the "Allow as recording rules target" toggle when configuring a data source.
# It only works if the data source's `jsonData.allowAsRecordingRulesTarget` prop does not contain a previously configured value.
;default_allow_recording_rules_target_alerts_ui_toggle = true
################################### SQL Data Sources #####################
[sql_datasources]
# Default maximum number of open connections maintained in the connection pool
@@ -61,6 +61,11 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#default_manage_alerts_ui_toggle
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#default_manage_alerts_ui_toggle
manage-recording-rules-toggle:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#default_allow_recording_rules_target_alerts_ui_toggle
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#default_allow_recording_rules_target_alerts_ui_toggle
private-data-source-connect:
- pattern: /docs/grafana/
destination: docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
@@ -81,6 +86,11 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#configuration-file-location
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#configuration-file-location
grafana-managed-recording-rules:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-recording-rules/create-grafana-managed-recording-rules/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-recording-rules/create-grafana-managed-recording-rules/
---
# Configure the Prometheus data source
@@ -179,6 +189,8 @@ Following are optional configuration settings you can configure for more control
- **Manage alerts via Alerting UI** -Toggled on by default. This enables [data source-managed rules in Grafana Alerting](ref:alerting-alert-rules) for this data source. For `Mimir`, it enables managing data source-managed rules and alerts. For `Prometheus`, it only supports viewing existing rules and alerts, which are displayed as data source-managed. Change this by setting the [`default_manage_alerts_ui_toggle`](ref:manage-alerts-toggle) option in the `grafana.ini` configuration file.
- **Allow as recording rules target** - Toggled on by default. This allows the data source to be selected as a target destination for writing [Grafana-managed recording rules](ref:grafana-managed-recording-rules). When enabled, this data source will appear in the target data source list when creating or importing recording rules. When disabled, the data source will be filtered out from recording rules target selection. Change this by setting the [`default_allow_recording_rules_target_alerts_ui_toggle`](ref:manage-recording-rules-toggle) option in the `grafana.ini` configuration file.
**Interval behavior:**
- **Scrape interval** - Sets the standard scrape and evaluation interval in Prometheus. The default is `15s`. This interval determines how often Prometheus scrapes targets. Set it to match the typical scrape and evaluation interval in your Prometheus configuration file. If you set a higher value than your Prometheus configuration, Grafana will evaluate data at this interval, resulting in fewer data points.
@@ -254,6 +266,7 @@ After you have provisioned a data source you cannot edit it.
jsonData:
httpMethod: POST
manageAlerts: true
allowAsRecordingRulesTarget: true
prometheusType: Prometheus
prometheusVersion: 3.3.0
cacheLevel: 'High'
@@ -888,6 +888,10 @@ Increasing this value allows processing more dashboards in each cleanup cycle bu
Default behavior for the "Manage alerts via Alerting UI" toggle when configuring a data source. It only works if the data source's `jsonData.manageAlerts` prop does not contain a previously configured value.
#### `default_allow_recording_rules_target_alerts_ui_toggle`
Default behavior for the "Allow as recording rules target" toggle when configuring a data source. It only works if the data source's `jsonData.allowAsRecordingRulesTarget` prop does not contain a previously configured value.
### `[sql_datasources]`
#### `max_open_conns_default`
@@ -642,6 +642,7 @@ export interface DataSourceJsonData {
defaultRegion?: string;
profile?: string;
manageAlerts?: boolean;
allowAsRecordingRulesTarget?: boolean;
alertmanagerUid?: string;
disableGrafanaCache?: boolean;
}
@@ -179,6 +179,9 @@ export const versionedComponents = {
manageAlerts: {
'10.4.0': 'prometheus-alerts-manager', // id for switch component
},
allowAsRecordingRulesTarget: {
'12.1.0': 'prometheus-recording-rules-target',
},
scrapeInterval: {
'10.4.0': 'data-testid scrape interval',
},
@@ -1,5 +1,6 @@
import { render } from '@testing-library/react';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { createDefaultConfigOptions } from '../test/__mocks__/datasource';
@@ -7,7 +8,7 @@ import { createDefaultConfigOptions } from '../test/__mocks__/datasource';
import { AlertingSettingsOverhaul } from './AlertingSettingsOverhaul';
describe(AlertingSettingsOverhaul.name, () => {
describe('Switch checked behavior', () => {
describe('Manage Alerts toggle', () => {
describe('when options.jsonData.manageAlerts is unset', () => {
it('uses the config default `true`', () => {
const options = createDefaultConfigOptions();
@@ -15,9 +16,12 @@ describe(AlertingSettingsOverhaul.name, () => {
config.defaultDatasourceManageAlertsUiToggle = true;
const { getByRole } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
expect(getByRole('switch')).toBeChecked();
const manageAlertsToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`
);
expect(manageAlertsToggle).toBeChecked();
});
it('uses the config default `false`', () => {
@@ -26,9 +30,12 @@ describe(AlertingSettingsOverhaul.name, () => {
config.defaultDatasourceManageAlertsUiToggle = false;
const { getByRole } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
expect(getByRole('switch')).not.toBeChecked();
const manageAlertsToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`
);
expect(manageAlertsToggle).not.toBeChecked();
});
});
@@ -39,9 +46,47 @@ describe(AlertingSettingsOverhaul.name, () => {
config.defaultDatasourceManageAlertsUiToggle = configDefault;
const { getByRole } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
expect(getByRole('switch')).toBeChecked();
const manageAlertsToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`
);
expect(manageAlertsToggle).toBeChecked();
});
});
});
describe('Recording Rules Target toggle', () => {
describe('when options.jsonData.allowAsRecordingRulesTarget is unset', () => {
it('defaults to `true` (enabled)', () => {
const options = createDefaultConfigOptions();
options.jsonData.allowAsRecordingRulesTarget = undefined;
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const recordingRulesTargetToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.allowAsRecordingRulesTarget}`
);
expect(recordingRulesTargetToggle).toBeChecked();
});
});
describe('when options.jsonData.allowAsRecordingRulesTarget is set', () => {
it.each([true, false])('uses the allowAsRecordingRulesTarget value %s', (value) => {
const options = createDefaultConfigOptions();
options.jsonData.allowAsRecordingRulesTarget = value;
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const recordingRulesTargetToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.allowAsRecordingRulesTarget}`
);
if (value) {
expect(recordingRulesTargetToggle).toBeChecked();
} else {
expect(recordingRulesTargetToggle).not.toBeChecked();
}
});
});
});
@@ -15,6 +15,7 @@ interface Props<T extends DataSourceJsonData>
interface AlertingConfig extends DataSourceJsonData {
manageAlerts?: boolean;
allowAsRecordingRulesTarget?: boolean;
}
export function AlertingSettingsOverhaul<T extends AlertingConfig>({
@@ -66,6 +67,41 @@ export function AlertingSettingsOverhaul<T extends AlertingConfig>({
</InlineField>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
labelWidth={30}
label={t(
'configuration.alerting-settings-overhaul.label-allow-as-recording-rules-target',
'Allow as recording rules target'
)}
disabled={options.readOnly}
tooltip={
<>
<Trans i18nKey="configuration.alerting-settings-overhaul.tooltip-allow-as-recording-rules-target">
Allow this data source to be selected as a target for writing recording rules.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
className={styles.switchField}
>
<Switch
value={
options.jsonData.allowAsRecordingRulesTarget ?? config.defaultAllowRecordingRulesTargetAlertsUiToggle
}
onChange={(event) =>
onOptionsChange({
...options,
jsonData: { ...options.jsonData, allowAsRecordingRulesTarget: event!.currentTarget.checked },
})
}
id={selectors.components.DataSource.Prometheus.configPage.allowAsRecordingRulesTarget}
/>
</InlineField>
</div>
</div>
</div>
</ConfigSubSection>
);
@@ -111,8 +111,10 @@
"configuration": {
"alerting-settings-overhaul": {
"label-manage-alerts-via-alerting-ui": "Manage alerts via Alerting UI",
"label-allow-as-recording-rules-target": "Allow as recording rules target",
"title-alerting": "Alerting",
"tooltip-manage-alerts-via-alerting-ui": "Manage alert rules for this data source. To manage other alerting resources, add an Alertmanager data source."
"tooltip-manage-alerts-via-alerting-ui": "Manage alert rules for this data source. To manage other alerting resources, add an Alertmanager data source.",
"tooltip-allow-as-recording-rules-target": "Allow this data source to be selected as a target for writing recording rules."
},
"config-editor": {
"browser-access-mode-error": "Browser access mode in the Prometheus data source is no longer available. Switch to server access mode.",
+1
View File
@@ -51,6 +51,7 @@ export interface PromOptions extends DataSourceJsonData {
incrementalQuerying?: boolean;
incrementalQueryOverlapWindow?: string;
disableRecordingRules?: boolean;
allowAsRecordingRulesTarget?: boolean;
sigV4Auth?: boolean;
oauthPassThru?: boolean;
codeModeMetricNamesSuggestionLimit?: number;
+1
View File
@@ -193,6 +193,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
connMaxLifetime: 14400,
};
defaultDatasourceManageAlertsUiToggle = true;
defaultAllowRecordingRulesTargetAlertsUiToggle = true;
tokenExpirationDayLimit: undefined;
enableFrontendSandboxForPlugins: string[] = [];
+2 -1
View File
@@ -249,7 +249,8 @@ type FrontendSettingsDTO struct {
Azure FrontendSettingsAzureDTO `json:"azure"`
DefaultDatasourceManageAlertsUIToggle bool `json:"defaultDatasourceManageAlertsUiToggle"`
DefaultDatasourceManageAlertsUIToggle bool `json:"defaultDatasourceManageAlertsUiToggle"`
DefaultAllowRecordingRulesTargetAlertsUIToggle bool `json:"defaultAllowRecordingRulesTargetAlertsUiToggle"`
Caching FrontendSettingsCachingDTO `json:"caching"`
RecordedQueries FrontendSettingsRecordedQueriesDTO `json:"recordedQueries"`
+2 -1
View File
@@ -261,7 +261,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
ExploreDefaultTimeOffset: hs.Cfg.ExploreDefaultTimeOffset,
ExploreHideLogsDownload: hs.Cfg.ExploreHideLogsDownload,
DefaultDatasourceManageAlertsUIToggle: hs.Cfg.DefaultDatasourceManageAlertsUIToggle,
DefaultDatasourceManageAlertsUIToggle: hs.Cfg.DefaultDatasourceManageAlertsUIToggle,
DefaultAllowRecordingRulesTargetAlertsUIToggle: hs.Cfg.DefaultAllowRecordingRulesTargetAlertsUIToggle,
BuildInfo: dtos.FrontendSettingsBuildInfoDTO{
HideVersion: hideVersion,
+4
View File
@@ -341,6 +341,9 @@ type Cfg struct {
// Default behavior for the "Manage alerts via Alerting UI" toggle when configuring a data source.
// It only works if the data source's `jsonData.manageAlerts` prop does not contain a previously configured value.
DefaultDatasourceManageAlertsUIToggle bool
// Default behavior for the "Allow as recording rules target" toggle when configuring a data source.
// It only works if the data source's `jsonData.allowAsRecordingRulesTarget` prop does not contain a previously configured value.
DefaultAllowRecordingRulesTargetAlertsUIToggle bool
// IP range access control
IPRangeACEnabled bool
@@ -1969,6 +1972,7 @@ func (cfg *Cfg) readDataSourcesSettings() {
cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000)
cfg.ConcurrentQueryCount = datasources.Key("concurrent_query_count").MustInt(10)
cfg.DefaultDatasourceManageAlertsUIToggle = datasources.Key("default_manage_alerts_ui_toggle").MustBool(true)
cfg.DefaultAllowRecordingRulesTargetAlertsUIToggle = datasources.Key("default_allow_recording_rules_target_alerts_ui_toggle").MustBool(true)
}
func (cfg *Cfg) readDataSourceSecuritySettings() {
@@ -25,7 +25,11 @@ import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/Neste
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { Folder } from '../../types/rule-form';
import { DataSourceType } from '../../utils/datasource';
import {
DataSourceType,
isSupportedExternalPrometheusFlavoredRulesSourceType,
isValidRecordingRulesTarget,
} from '../../utils/datasource';
import { stringifyErrorLike } from '../../utils/misc';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertingPageWrapper } from '../AlertingPageWrapper';
@@ -319,11 +323,11 @@ function YamlTargetDataSourceField() {
noDefault
inputId="yaml-target-data-source"
alerting
filter={(ds: DataSourceInstanceSettings) => ds.type === 'prometheus'}
filter={(ds: DataSourceInstanceSettings) => isSupportedExternalPrometheusFlavoredRulesSourceType(ds.type)}
onChange={(ds: DataSourceInstanceSettings) => {
setValue('yamlImportTargetDatasourceUID', ds.uid);
const recordingRulesTargetDs = getValues('targetDatasourceUID');
if (!recordingRulesTargetDs) {
if (!recordingRulesTargetDs && isValidRecordingRulesTarget(ds)) {
setValue('targetDatasourceUID', ds.uid);
}
}}
@@ -367,7 +371,7 @@ function TargetDataSourceForRecordingRulesField() {
current={field.value}
inputId="recording-rules-target-data-source"
noDefault
filter={(ds: DataSourceInstanceSettings) => ds.type === 'prometheus'}
filter={isValidRecordingRulesTarget}
onChange={(ds: DataSourceInstanceSettings) => {
setValue('targetDatasourceUID', ds.uid);
}}
@@ -479,7 +483,7 @@ function DataSourceField() {
// If we've chosen a Prometheus data source, we can set the recording rules target data source to the same as the source
const recordingRulesTargetDs = getValues('targetDatasourceUID');
if (!recordingRulesTargetDs) {
const targetDataSourceUID = ds.type === DataSourceType.Prometheus ? ds.uid : undefined;
const targetDataSourceUID = isValidRecordingRulesTarget(ds) ? ds.uid : undefined;
setValue('targetDatasourceUID', targetDataSourceUID);
}
}}
@@ -7,7 +7,7 @@ import { Field, Input, Stack, Text } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { isSupportedExternalPrometheusFlavoredRulesSourceType } from '../../utils/datasource';
import { isValidRecordingRulesTarget } from '../../utils/datasource';
import { isCloudRecordingRuleByType, isGrafanaRecordingRuleByType, isRecordingRuleByType } from '../../utils/rules';
import { RuleEditorSection } from './RuleEditorSection';
@@ -128,9 +128,7 @@ export const AlertRuleNameAndMetric = () => {
current={field.value}
noDefault
// Filter with `filter` prop instead of `type` prop to avoid showing the `-- Grafana --` data source
filter={(ds: DataSourceInstanceSettings) =>
isSupportedExternalPrometheusFlavoredRulesSourceType(ds.type)
}
filter={isValidRecordingRulesTarget}
onChange={(ds: DataSourceInstanceSettings) => {
setValue('targetDatasourceUid', ds.uid);
}}
@@ -1,6 +1,10 @@
import { mockDataSource } from '../mocks';
import { isDataSourceManagingAlerts } from './datasource';
import {
SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES,
isDataSourceManagingAlerts,
isValidRecordingRulesTarget,
} from './datasource';
describe('isDataSourceManagingAlerts', () => {
it('should return true when the prop is set as true', () => {
@@ -37,3 +41,48 @@ it('should return false when the prop is set as false', () => {
)
).toBe(false);
});
describe('isValidRecordingRulesTarget', () => {
it.each(SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES)(
'should return true for %s datasource with manageRecordingRulesTarget enabled',
(type) => {
expect(
isValidRecordingRulesTarget(
mockDataSource({
type,
jsonData: {
allowAsRecordingRulesTarget: true,
},
})
)
).toBe(true);
}
);
it.each(SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES)(
'should return true for %s datasource when manageRecordingRulesTarget is undefined (defaults to true)',
(type) => {
expect(
isValidRecordingRulesTarget(
mockDataSource({
type,
jsonData: {},
})
)
).toBe(true);
}
);
it('should return false for loki datasource (unsupported type)', () => {
expect(
isValidRecordingRulesTarget(
mockDataSource({
type: 'loki',
jsonData: {
allowAsRecordingRulesTarget: true,
},
})
)
).toBe(false);
});
});
@@ -338,6 +338,10 @@ export function isDataSourceManagingAlerts(ds: DataSourceInstanceSettings<DataSo
return ds.jsonData.manageAlerts !== false; //if this prop is undefined it defaults to true
}
export function isDataSourceAllowedAsRecordingRulesTarget(ds: DataSourceInstanceSettings<DataSourceJsonData>) {
return ds.jsonData.allowAsRecordingRulesTarget !== false; // if this prop is undefined it defaults to true
}
export function ruleIdentifierToRuleSourceIdentifier(ruleIdentifier: RuleIdentifier): RulesSourceIdentifier {
if (isGrafanaRuleIdentifier(ruleIdentifier)) {
return { uid: GrafanaRulesSourceSymbol, name: GRAFANA_RULES_SOURCE_NAME, ruleSourceType: 'grafana' };
@@ -389,3 +393,7 @@ export const SUPPORTED_RULE_SOURCE_TYPES = [
GRAFANA_RULES_SOURCE_NAME,
...SUPPORTED_EXTERNAL_RULE_SOURCE_TYPES,
] as const satisfies string[];
export function isValidRecordingRulesTarget(ds: DataSourceInstanceSettings<DataSourceJsonData>): boolean {
return isSupportedExternalPrometheusFlavoredRulesSourceType(ds.type) && isDataSourceAllowedAsRecordingRulesTarget(ds);
}