Alerting: Add alertingUIUseFullyCompatBackendFilters feature toggle (#114153)
* Add alertingUIUseFullyCompatBackendFilters for fully compatible backend filters * Remove hide from admin flag
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Generated
+1
@@ -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
|
||||
|
||||
|
Generated
+4
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user