Alerting: Add datasource_uid query param to search for rules (#114697)
* Alerting: Add datasource param to BE search * use array for param, datasource -> datasources * tests * remove comments * tests, short-circuit request if all data source names are invalid * rephrase comment * update some tests... * make linter happy * datasource_uid -> datasource_uids * added test * datasource_uids -> datasource_uid --------- Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
This commit is contained in:
co-authored by
Sonia Aguilar
parent
f4fbbcc4f4
commit
73b9a8c3af
@@ -37,6 +37,7 @@ type PromRulesOptions = WithNotificationOptions<{
|
||||
export type GrafanaPromRulesOptions = Omit<PromRulesOptions, 'ruleSource' | 'namespace' | 'excludeAlerts'> & {
|
||||
folderUid?: string;
|
||||
dashboardUid?: string;
|
||||
datasources?: string[];
|
||||
panelId?: number;
|
||||
limitAlerts?: number;
|
||||
ruleLimit?: number;
|
||||
@@ -98,6 +99,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
limitAlerts,
|
||||
groupNextToken,
|
||||
title,
|
||||
datasources,
|
||||
searchGroupName,
|
||||
dashboardUid,
|
||||
}) => ({
|
||||
@@ -114,6 +116,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
rule_limit: ruleLimit?.toFixed(0),
|
||||
group_limit: groupLimit?.toFixed(0),
|
||||
group_next_token: groupNextToken,
|
||||
datasource_uid: datasources,
|
||||
'search.rule_name': title,
|
||||
'search.rule_group': searchGroupName,
|
||||
dashboard_uid: dashboardUid,
|
||||
|
||||
@@ -259,7 +259,7 @@ function looseParseMatcher(matcherQuery: string): Matcher | undefined {
|
||||
}
|
||||
|
||||
// Memoize the function to avoid calling getDatasourceAPIUid for the filter values multiple times
|
||||
const mapDataSourceNamesToUids = memoize(
|
||||
export const mapDataSourceNamesToUids = memoize(
|
||||
(names: string[]): string[] => {
|
||||
return names.map((name) => attempt(getDatasourceAPIUid, name)).filter(isString);
|
||||
},
|
||||
|
||||
@@ -257,12 +257,55 @@ describe('grafana-managed rules', () => {
|
||||
expect(backendFilter.health).toEqual([]);
|
||||
expect(backendFilter.contactPoint).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not set hasInvalidDataSourceNames flag when no data source names are provided', () => {
|
||||
const { hasInvalidDataSourceNames } = getGrafanaFilter(getFilter({}));
|
||||
|
||||
expect(hasInvalidDataSourceNames).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backend filtering with alertingUIUseBackendFilters feature toggle', () => {
|
||||
describe('when alertingUIUseBackendFilters is enabled', () => {
|
||||
testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters'] });
|
||||
|
||||
it('should include datasources in backend filter when valid data source names are provided', () => {
|
||||
const { backendFilter, hasInvalidDataSourceNames } = getGrafanaFilter(
|
||||
getFilter({ dataSourceNames: ['prometheus', 'loki'] })
|
||||
);
|
||||
|
||||
expect(backendFilter.datasources).toEqual(['datasource-uid-1', 'datasource-uid-3']);
|
||||
expect(hasInvalidDataSourceNames).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect invalid data source names and set hasInvalidDataSourceNames flag', () => {
|
||||
const { backendFilter, hasInvalidDataSourceNames } = getGrafanaFilter(
|
||||
getFilter({ dataSourceNames: ['non-existent-datasource'] })
|
||||
);
|
||||
|
||||
expect(backendFilter.datasources).toEqual([]);
|
||||
expect(hasInvalidDataSourceNames).toBe(true);
|
||||
});
|
||||
|
||||
it('should include only valid datasource UIDs when some names are invalid', () => {
|
||||
const { backendFilter, hasInvalidDataSourceNames } = getGrafanaFilter(
|
||||
getFilter({ dataSourceNames: ['prometheus', 'non-existent-datasource'] })
|
||||
);
|
||||
|
||||
expect(backendFilter.datasources).toEqual(['datasource-uid-1']);
|
||||
expect(hasInvalidDataSourceNames).toBe(false); // Not all are invalid
|
||||
});
|
||||
|
||||
it('should skip dataSourceNames filtering on frontend when backend filtering is enabled', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1'],
|
||||
});
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ dataSourceNames: ['loki'] }));
|
||||
// Should return true because dataSourceNames filter is null (handled by backend).
|
||||
expect(frontendFilter.ruleMatches(rule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should include title in backend filter when freeFormWords are provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ freeFormWords: ['cpu', 'usage'] }));
|
||||
|
||||
@@ -556,25 +599,32 @@ describe('grafana-managed rules', () => {
|
||||
const { frontendFilter: groupNoMatch } = getGrafanaFilter(getFilter({ groupName: 'memory' }));
|
||||
expect(groupNoMatch.groupMatches(group)).toBe(false);
|
||||
|
||||
// Always-frontend filters (labels, dataSourceNames, namespace) should work
|
||||
// Always-frontend filters (labels, 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);
|
||||
});
|
||||
|
||||
it('should skip dataSourceNames filtering on frontend (handled by backend)', () => {
|
||||
const alertingRule = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1'],
|
||||
});
|
||||
|
||||
// DataSourceNames is backend-filtered when feature toggle is enabled.
|
||||
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(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when both alertingUIUseBackendFilters and alertingUIUseFullyCompatBackendFilters are enabled', () => {
|
||||
@@ -631,11 +681,10 @@ describe('grafana-managed rules', () => {
|
||||
expect(frontendFilter.groupMatches(group)).toBe(true);
|
||||
});
|
||||
|
||||
it('should still apply always-frontend filters (labels, dataSourceNames, namespace)', () => {
|
||||
it('should still apply always-frontend filters (labels, namespace)', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({
|
||||
name: 'High CPU Usage',
|
||||
labels: { severity: 'critical' },
|
||||
queriedDatasourceUIDs: ['datasource-uid-1'],
|
||||
alerts: [],
|
||||
});
|
||||
|
||||
@@ -646,13 +695,6 @@ describe('grafana-managed rules', () => {
|
||||
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',
|
||||
@@ -667,6 +709,19 @@ describe('grafana-managed rules', () => {
|
||||
const { frontendFilter: nsFilter2 } = getGrafanaFilter(getFilter({ namespace: 'staging' }));
|
||||
expect(nsFilter2.groupMatches(group)).toBe(false);
|
||||
});
|
||||
|
||||
it('should skip dataSourceNames filtering on frontend (handled by backend)', () => {
|
||||
const rule = mockGrafanaPromAlertingRule({
|
||||
queriedDatasourceUIDs: ['datasource-uid-1'],
|
||||
});
|
||||
|
||||
// DataSourceNames is backend-filtered when both feature toggles are enabled.
|
||||
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(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -732,9 +787,12 @@ describe('grafana-managed rules', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for dataSourceNames (handled by backend when feature toggle is enabled)', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for client-side only filters', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -749,10 +807,11 @@ describe('grafana-managed rules', () => {
|
||||
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
|
||||
// Should return false for: empty, backend-handled (ruleType, dashboardUid, dataSourceNames), and backend-only filters
|
||||
expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false);
|
||||
@@ -762,7 +821,6 @@ describe('grafana-managed rules', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -782,10 +840,12 @@ describe('grafana-managed rules', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false);
|
||||
|
||||
// Should return true for: always-frontend filters only
|
||||
// Should return true for: always-frontend filters only (namespace, labels)
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true);
|
||||
|
||||
// Should return false for: backend-handled dataSourceNames when feature toggles are enabled
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
groupMatches,
|
||||
groupNameFilter,
|
||||
labelsFilter,
|
||||
mapDataSourceNamesToUids,
|
||||
namespaceFilter,
|
||||
pluginsFilter,
|
||||
ruleMatches,
|
||||
@@ -63,6 +64,17 @@ export function getGrafanaFilter(filterState: Partial<RulesFilter>) {
|
||||
// Build title search for backend filtering
|
||||
const titleSearch = buildTitleSearch(normalizedFilterState);
|
||||
|
||||
// Check if data source names were provided but none are valid.
|
||||
let hasInvalidDataSourceNames = false;
|
||||
let datasourceUids: string[] | undefined = undefined;
|
||||
|
||||
// Only map datasources if data source filter should be applied on backend (when ruleFilterConfig.dataSourceNames is null).
|
||||
if (ruleFilterConfig.dataSourceNames === null && normalizedFilterState.dataSourceNames.length > 0) {
|
||||
datasourceUids = mapDataSourceNamesToUids(normalizedFilterState.dataSourceNames);
|
||||
// If names were provided but no valid UIDs were found, all names are invalid.
|
||||
hasInvalidDataSourceNames = datasourceUids.length === 0;
|
||||
}
|
||||
|
||||
const backendFilter: GrafanaPromRulesOptions = {
|
||||
state: normalizedFilterState.ruleState ? [normalizedFilterState.ruleState] : [],
|
||||
health: normalizedFilterState.ruleHealth ? [normalizedFilterState.ruleHealth] : [],
|
||||
@@ -72,6 +84,7 @@ export function getGrafanaFilter(filterState: Partial<RulesFilter>) {
|
||||
type: ruleFilterConfig.ruleType ? undefined : normalizedFilterState.ruleType,
|
||||
dashboardUid: ruleFilterConfig.dashboardUid ? undefined : normalizedFilterState.dashboardUid,
|
||||
searchGroupName: groupFilterConfig.groupName ? undefined : normalizedFilterState.groupName,
|
||||
datasources: ruleFilterConfig.dataSourceNames ? undefined : datasourceUids,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -80,6 +93,7 @@ export function getGrafanaFilter(filterState: Partial<RulesFilter>) {
|
||||
groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, groupFilterConfig),
|
||||
ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, ruleFilterConfig),
|
||||
},
|
||||
hasInvalidDataSourceNames,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,7 +114,7 @@ function buildGrafanaFilterConfigs() {
|
||||
ruleName: useBackendFilters ? null : ruleNameFilter,
|
||||
ruleState: null,
|
||||
ruleType: useBackendFilters || useFullyCompatibleBackendFilters ? null : ruleTypeFilter,
|
||||
dataSourceNames: dataSourceNamesFilter,
|
||||
dataSourceNames: useBackendFilters || useFullyCompatibleBackendFilters ? null : dataSourceNamesFilter,
|
||||
labels: labelsFilter,
|
||||
ruleHealth: null,
|
||||
dashboardUid: useBackendFilters || useFullyCompatibleBackendFilters ? null : dashboardUidFilter,
|
||||
|
||||
@@ -84,7 +84,12 @@ export function useFilteredRulesIteratorProvider() {
|
||||
|
||||
const hasDataSourceFilterActive = Boolean(filterState.dataSourceNames.length);
|
||||
|
||||
const { backendFilter, frontendFilter } = getGrafanaFilter(filterState);
|
||||
const { backendFilter, frontendFilter, hasInvalidDataSourceNames } = getGrafanaFilter(filterState);
|
||||
|
||||
// Short-circuit: if all provided data source names are invalid, return empty results (no rules can match).
|
||||
if (hasInvalidDataSourceNames) {
|
||||
return { iterable: empty(), abortController };
|
||||
}
|
||||
|
||||
const grafanaRulesGenerator: AsyncIterableX<RuleWithOrigin> = from(
|
||||
grafanaGroupsGenerator(options.grafanaManagedLimit, backendFilter)
|
||||
|
||||
@@ -73,6 +73,7 @@ describe('paginationLimits', () => {
|
||||
{ ruleState: PromAlertingRuleState.Firing },
|
||||
{ ruleHealth: RuleHealth.Ok },
|
||||
{ contactPoint: 'slack' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
])(
|
||||
'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p',
|
||||
(filterState) => {
|
||||
@@ -85,7 +86,6 @@ describe('paginationLimits', () => {
|
||||
|
||||
it.each<Partial<RulesFilter>>([
|
||||
{ namespace: 'production' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
{ labels: ['severity=critical'] },
|
||||
{ ruleState: PromAlertingRuleState.Firing, namespace: 'production' },
|
||||
])('should return large limits for both when frontend filters are used: %p', (filterState) => {
|
||||
@@ -112,6 +112,7 @@ describe('paginationLimits', () => {
|
||||
{ ruleState: PromAlertingRuleState.Firing },
|
||||
{ ruleHealth: RuleHealth.Ok },
|
||||
{ contactPoint: 'slack' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
])(
|
||||
'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p',
|
||||
(filterState) => {
|
||||
@@ -127,7 +128,6 @@ describe('paginationLimits', () => {
|
||||
{ ruleName: 'alert' },
|
||||
{ groupName: 'test-group' },
|
||||
{ namespace: 'production' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
{ labels: ['severity=critical'] },
|
||||
])('should return large limits for both when frontend filters are used: %p', (filterState) => {
|
||||
const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState));
|
||||
@@ -156,6 +156,7 @@ describe('paginationLimits', () => {
|
||||
{ ruleState: PromAlertingRuleState.Firing },
|
||||
{ ruleHealth: RuleHealth.Ok },
|
||||
{ contactPoint: 'slack' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
])(
|
||||
'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p',
|
||||
(filterState) => {
|
||||
@@ -166,16 +167,15 @@ describe('paginationLimits', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it.each<Partial<RulesFilter>>([
|
||||
{ namespace: 'production' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
{ labels: ['severity=critical'] },
|
||||
])('should return large limits for both when frontend filters are used: %p', (filterState) => {
|
||||
const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState));
|
||||
it.each<Partial<RulesFilter>>([{ namespace: 'production' }, { labels: ['severity=critical'] }])(
|
||||
'should return large limits for both when frontend filters are used: %p',
|
||||
(filterState) => {
|
||||
const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState));
|
||||
|
||||
expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
});
|
||||
expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user