Alerting: Add alertingUIUseFullyCompatBackendFilters feature toggle (#114153)

* Add alertingUIUseFullyCompatBackendFilters for fully compatible backend filters

* Remove hide from admin flag
This commit is contained in:
Konrad Lalik
2025-11-24 12:42:09 +01:00
committed by GitHub
parent b5335d9357
commit 01a4a5409a
8 changed files with 289 additions and 13 deletions
+4
View File
@@ -329,6 +329,10 @@ export interface FeatureToggles {
*/
alertingUIUseBackendFilters?: boolean;
/**
* Enables the UI to use rules backend-side filters 100% compatible with the frontend filters
*/
alertingUIUseFullyCompatBackendFilters?: boolean;
/**
* Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.
*/
alertmanagerRemotePrimary?: boolean;
+7
View File
@@ -527,6 +527,13 @@ var (
Owner: grafanaAlertingSquad,
HideFromDocs: true,
},
{
Name: "alertingUIUseFullyCompatBackendFilters",
Description: "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
HideFromDocs: true,
},
{
Name: "alertmanagerRemotePrimary",
Description: "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.",
+1
View File
@@ -73,6 +73,7 @@ cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-e
alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false
alertingProvenanceLockWrites,experimental,@grafana/alerting-squad,false,false,false
alertingUIUseBackendFilters,experimental,@grafana/alerting-squad,false,false,false
alertingUIUseFullyCompatBackendFilters,experimental,@grafana/alerting-squad,false,false,false
alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false
annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
73 alertmanagerRemoteSecondary experimental @grafana/alerting-squad false false false
74 alertingProvenanceLockWrites experimental @grafana/alerting-squad false false false
75 alertingUIUseBackendFilters experimental @grafana/alerting-squad false false false
76 alertingUIUseFullyCompatBackendFilters experimental @grafana/alerting-squad false false false
77 alertmanagerRemotePrimary experimental @grafana/alerting-squad false false false
78 annotationPermissionUpdate GA @grafana/identity-access-team false false false
79 dashboardSceneForViewers GA @grafana/dashboards-squad false false true
+4
View File
@@ -243,6 +243,10 @@ const (
// Enables the UI to use certain backend-side filters
FlagAlertingUIUseBackendFilters = "alertingUIUseBackendFilters"
// FlagAlertingUIUseFullyCompatBackendFilters
// Enables the UI to use rules backend-side filters 100% compatible with the frontend filters
FlagAlertingUIUseFullyCompatBackendFilters = "alertingUIUseFullyCompatBackendFilters"
// FlagAlertmanagerRemotePrimary
// Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.
FlagAlertmanagerRemotePrimary = "alertmanagerRemotePrimary"
+29
View File
@@ -540,6 +540,35 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "alertingUIUseCompatibleBackendFilters",
"resourceVersion": "1763478911684",
"creationTimestamp": "2025-11-18T15:15:11Z",
"deletionTimestamp": "2025-11-18T15:17:12Z"
},
"spec": {
"description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "alertingUIUseFullyCompatBackendFilters",
"resourceVersion": "1763479032988",
"creationTimestamp": "2025-11-18T15:17:12Z"
},
"spec": {
"description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "alertingUseNewSimplifiedRoutingHashAlgorithm",
@@ -23,3 +23,6 @@ export const shouldAllowPermanentlyDeletingRules = () =>
(shouldAllowRecoveringDeletedRules() && config.featureToggles.alertingRulePermanentlyDelete) ?? false;
export const shouldUseBackendFilters = () => config.featureToggles.alertingUIUseBackendFilters ?? false;
export const shouldUseFullyCompatibleBackendFilters = () =>
config.featureToggles.alertingUIUseFullyCompatBackendFilters ?? false;
@@ -484,6 +484,190 @@ describe('grafana-managed rules', () => {
expect(frontendFilter2.groupMatches(group)).toBe(false);
});
});
describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => {
testWithFeatureToggles({ enable: ['alertingUIUseFullyCompatBackendFilters'] });
it('should populate backend filters correctly (ruleType, dashboardUid)', () => {
// Fully compatible filters should be in backend
const { backendFilter } = getGrafanaFilter(
getFilter({
ruleType: PromRuleType.Alerting,
dashboardUid: 'dashboard-123',
freeFormWords: ['cpu'],
groupName: 'my-group',
})
);
expect(backendFilter.type).toBe(PromRuleType.Alerting);
expect(backendFilter.dashboardUid).toBe('dashboard-123');
// Non-compatible filters should NOT be in backend
expect(backendFilter.title).toBeUndefined();
expect(backendFilter.searchGroupName).toBeUndefined();
// Empty state
const { backendFilter: emptyFilter } = getGrafanaFilter(getFilter({}));
expect(emptyFilter.type).toBeUndefined();
expect(emptyFilter.dashboardUid).toBeUndefined();
});
it('should apply frontend filters correctly', () => {
const alertingRule = mockGrafanaPromAlertingRule({
name: 'High CPU Usage',
labels: { severity: 'critical' },
queriedDatasourceUIDs: ['datasource-uid-1'],
annotations: { [Annotation.dashboardUID]: 'dashboard-a' },
alerts: [],
});
const recordingRule = mockPromRecordingRule({ name: 'Test Recording' });
// Backend-handled filters (ruleType, dashboardUid) should skip frontend filtering
const { frontendFilter: backendHandledFilter } = getGrafanaFilter(
getFilter({ ruleType: PromRuleType.Recording, dashboardUid: 'dashboard-b' })
);
expect(backendHandledFilter.ruleMatches(alertingRule)).toBe(true);
expect(backendHandledFilter.ruleMatches(recordingRule)).toBe(true);
// Frontend-handled filters (freeFormWords, ruleName) should work
const { frontendFilter: freeFormMatch } = getGrafanaFilter(getFilter({ freeFormWords: ['cpu'] }));
expect(freeFormMatch.ruleMatches(alertingRule)).toBe(true);
const { frontendFilter: freeFormNoMatch } = getGrafanaFilter(getFilter({ freeFormWords: ['memory'] }));
expect(freeFormNoMatch.ruleMatches(alertingRule)).toBe(false);
const { frontendFilter: ruleNameMatch } = getGrafanaFilter(getFilter({ ruleName: 'cpu' }));
expect(ruleNameMatch.ruleMatches(alertingRule)).toBe(true);
const { frontendFilter: ruleNameNoMatch } = getGrafanaFilter(getFilter({ ruleName: 'memory' }));
expect(ruleNameNoMatch.ruleMatches(alertingRule)).toBe(false);
// Group name filtering
const group: PromRuleGroupDTO = {
name: 'CPU Usage Alerts',
file: 'production/alerts',
rules: [],
interval: 60,
};
const { frontendFilter: groupMatch } = getGrafanaFilter(getFilter({ groupName: 'cpu' }));
expect(groupMatch.groupMatches(group)).toBe(true);
const { frontendFilter: groupNoMatch } = getGrafanaFilter(getFilter({ groupName: 'memory' }));
expect(groupNoMatch.groupMatches(group)).toBe(false);
// Always-frontend filters (labels, dataSourceNames, namespace) should work
const { frontendFilter: labelsMatch } = getGrafanaFilter(getFilter({ labels: ['severity=critical'] }));
expect(labelsMatch.ruleMatches(alertingRule)).toBe(true);
const { frontendFilter: labelsNoMatch } = getGrafanaFilter(getFilter({ labels: ['severity=warning'] }));
expect(labelsNoMatch.ruleMatches(alertingRule)).toBe(false);
const { frontendFilter: dsMatch } = getGrafanaFilter(getFilter({ dataSourceNames: ['prometheus'] }));
expect(dsMatch.ruleMatches(alertingRule)).toBe(true);
const { frontendFilter: dsNoMatch } = getGrafanaFilter(getFilter({ dataSourceNames: ['loki'] }));
expect(dsNoMatch.ruleMatches(alertingRule)).toBe(false);
const { frontendFilter: nsMatch } = getGrafanaFilter(getFilter({ namespace: 'production' }));
expect(nsMatch.groupMatches(group)).toBe(true);
const { frontendFilter: nsNoMatch } = getGrafanaFilter(getFilter({ namespace: 'staging' }));
expect(nsNoMatch.groupMatches(group)).toBe(false);
});
});
describe('when both alertingUIUseBackendFilters and alertingUIUseFullyCompatBackendFilters are enabled', () => {
testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] });
it('should include all backend filters (title, ruleType, dashboardUid, searchGroupName)', () => {
const { backendFilter } = getGrafanaFilter(
getFilter({
freeFormWords: ['cpu'],
ruleName: 'alert',
ruleType: PromRuleType.Alerting,
dashboardUid: 'dashboard-123',
groupName: 'my-group',
})
);
expect(backendFilter.title).toBe('alert cpu');
expect(backendFilter.type).toBe(PromRuleType.Alerting);
expect(backendFilter.dashboardUid).toBe('dashboard-123');
expect(backendFilter.searchGroupName).toBe('my-group');
});
it('should skip all backend-handled filters on frontend', () => {
const alertingRule = mockGrafanaPromAlertingRule({
name: 'High CPU Usage',
annotations: { [Annotation.dashboardUID]: 'dashboard-a' },
});
const recordingRule = mockPromRecordingRule({ name: 'Test Recording' });
const { frontendFilter } = getGrafanaFilter(
getFilter({
freeFormWords: ['memory'],
ruleName: 'memory',
ruleType: PromRuleType.Recording,
dashboardUid: 'dashboard-b',
})
);
// All these filters are handled by backend, so frontend should return true
expect(frontendFilter.ruleMatches(alertingRule)).toBe(true);
expect(frontendFilter.ruleMatches(recordingRule)).toBe(true);
});
it('should skip groupName filtering on frontend', () => {
const group: PromRuleGroupDTO = {
name: 'CPU Usage Alerts',
file: 'production/alerts',
rules: [],
interval: 60,
};
const { frontendFilter } = getGrafanaFilter(getFilter({ groupName: 'memory' }));
// Should return true because groupName filter is null (handled by backend)
expect(frontendFilter.groupMatches(group)).toBe(true);
});
it('should still apply always-frontend filters (labels, dataSourceNames, namespace)', () => {
const rule = mockGrafanaPromAlertingRule({
name: 'High CPU Usage',
labels: { severity: 'critical' },
queriedDatasourceUIDs: ['datasource-uid-1'],
alerts: [],
});
// Labels filter should still work
const { frontendFilter: labelFilter } = getGrafanaFilter(getFilter({ labels: ['severity=warning'] }));
expect(labelFilter.ruleMatches(rule)).toBe(false);
const { frontendFilter: labelFilter2 } = getGrafanaFilter(getFilter({ labels: ['severity=critical'] }));
expect(labelFilter2.ruleMatches(rule)).toBe(true);
// DataSourceNames filter should still work
const { frontendFilter: dsFilter } = getGrafanaFilter(getFilter({ dataSourceNames: ['prometheus'] }));
expect(dsFilter.ruleMatches(rule)).toBe(true);
const { frontendFilter: dsFilter2 } = getGrafanaFilter(getFilter({ dataSourceNames: ['loki'] }));
expect(dsFilter2.ruleMatches(rule)).toBe(false);
// Namespace filter should still work
const group: PromRuleGroupDTO = {
name: 'Test Group',
file: 'production/alerts',
rules: [],
interval: 60,
};
const { frontendFilter: nsFilter } = getGrafanaFilter(getFilter({ namespace: 'production' }));
expect(nsFilter.groupMatches(group)).toBe(true);
const { frontendFilter: nsFilter2 } = getGrafanaFilter(getFilter({ namespace: 'staging' }));
expect(nsFilter2.groupMatches(group)).toBe(false);
});
});
});
describe('hasClientSideFilters', () => {
@@ -560,5 +744,49 @@ describe('grafana-managed rules', () => {
expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false);
});
});
describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => {
testWithFeatureToggles({ enable: ['alertingUIUseFullyCompatBackendFilters'] });
it('should return correct values for all filter types', () => {
// Should return false for: empty, backend-handled (ruleType, dashboardUid), and backend-only filters
expect(hasClientSideFilters(getFilter({}))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false);
expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false);
expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false);
// Should return true for: frontend-handled filters
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true);
});
});
describe('when both alertingUIUseBackendFilters and alertingUIUseFullyCompatBackendFilters are enabled', () => {
testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] });
it('should return correct values for all filter types', () => {
// Should return false for: empty, all backend-handled filters, and backend-only filters
expect(hasClientSideFilters(getFilter({}))).toBe(false);
expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false);
expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false);
expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false);
expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false);
expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false);
// Should return true for: always-frontend filters only
expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true);
expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true);
});
});
});
});
@@ -1,7 +1,7 @@
import { PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
import { GrafanaPromRulesOptions } from '../../api/prometheusApi';
import { shouldUseBackendFilters } from '../../featureToggles';
import { shouldUseBackendFilters, shouldUseFullyCompatibleBackendFilters } from '../../featureToggles';
import { RulesFilter } from '../../search/rulesSearchParser';
import { buildTitleSearch, normalizeFilterState } from './filterNormalization';
@@ -57,7 +57,8 @@ export function hasClientSideFilters(filterState: RulesFilter): boolean {
*/
export function getGrafanaFilter(filterState: RulesFilter) {
const normalizedFilterState = normalizeFilterState(filterState);
const useBackendFilters = shouldUseBackendFilters();
const { ruleFilterConfig, groupFilterConfig } = buildGrafanaFilterConfigs();
// Build title search for backend filtering
const titleSearch = buildTitleSearch(normalizedFilterState);
@@ -66,20 +67,18 @@ export function getGrafanaFilter(filterState: RulesFilter) {
state: normalizedFilterState.ruleState ? [normalizedFilterState.ruleState] : [],
health: normalizedFilterState.ruleHealth ? [normalizedFilterState.ruleHealth] : [],
contactPoint: normalizedFilterState.contactPoint ?? undefined,
title: useBackendFilters ? titleSearch : undefined,
type: useBackendFilters ? normalizedFilterState.ruleType : undefined,
dashboardUid: useBackendFilters ? normalizedFilterState.dashboardUid : undefined,
searchGroupName: useBackendFilters ? normalizedFilterState.groupName : undefined,
// If FE filter is defined, don't include the backend filter
title: ruleFilterConfig.ruleName ? undefined : titleSearch,
type: ruleFilterConfig.ruleType ? undefined : normalizedFilterState.ruleType,
dashboardUid: ruleFilterConfig.dashboardUid ? undefined : normalizedFilterState.dashboardUid,
searchGroupName: groupFilterConfig.groupName ? undefined : normalizedFilterState.groupName,
};
const { ruleFilterConfig: grafanaFilterProcessingConfig, groupFilterConfig: grafanaGroupFilterConfig } =
buildGrafanaFilterConfigs();
return {
backendFilter,
frontendFilter: {
groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, grafanaGroupFilterConfig),
ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, grafanaFilterProcessingConfig),
groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, groupFilterConfig),
ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, ruleFilterConfig),
},
};
}
@@ -93,17 +92,18 @@ export function getGrafanaFilter(filterState: RulesFilter) {
*/
function buildGrafanaFilterConfigs() {
const useBackendFilters = shouldUseBackendFilters();
const useFullyCompatibleBackendFilters = shouldUseFullyCompatibleBackendFilters();
const ruleFilterConfig: RuleFilterConfig = {
// When backend filtering is enabled, these filters are handled by the backend
freeFormWords: useBackendFilters ? null : freeFormFilter,
ruleName: useBackendFilters ? null : ruleNameFilter,
ruleState: null,
ruleType: useBackendFilters ? null : ruleTypeFilter,
ruleType: useBackendFilters || useFullyCompatibleBackendFilters ? null : ruleTypeFilter,
dataSourceNames: dataSourceNamesFilter,
labels: labelsFilter,
ruleHealth: null,
dashboardUid: useBackendFilters ? null : dashboardUidFilter,
dashboardUid: useBackendFilters || useFullyCompatibleBackendFilters ? null : dashboardUidFilter,
plugins: pluginsFilter,
contactPoint: null,
};