From f79f6efdcb409968f0c78462bf43d01976e6bcf8 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Tue, 6 May 2025 14:09:29 +0200 Subject: [PATCH] Alerting: Use Prometheus endpoint as a primary source of data on the new list page (#104538) * Bring back contact points filter * Use GMA Prometheus endpoint as main source of truth on the list page * refactor: improve error handling in GrafanaRuleLoader --- .../alerting/unified/api/alertRuleApi.ts | 13 ++++++ .../alerting/unified/api/alertingApi.ts | 1 + .../alerting/unified/api/prometheusApi.ts | 28 +++++++++++++ .../rules/Filter/RulesFilter.v1.tsx | 7 +--- public/app/features/alerting/unified/mocks.ts | 1 + .../alerting/unified/rule-list/FilterView.tsx | 2 +- .../rule-list/GrafanaGroupLoader.test.tsx | 1 + .../unified/rule-list/GrafanaGroupLoader.tsx | 9 ++++ .../unified/rule-list/GrafanaRuleLoader.tsx | 42 ++++++++++++------- .../components/AlertRuleListItemLoader.tsx | 24 +++++++---- .../unified/rule-list/hooks/filters.ts | 14 +++++++ .../hooks/useFilteredRulesIterator.ts | 7 +++- .../fixtures/alertRules.fixture.ts | 1 + public/app/types/unified-alerting-dto.ts | 2 + public/locales/en-US/grafana.json | 4 +- 15 files changed, 123 insertions(+), 33 deletions(-) diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index 1b9f006ad8c..9127ae5a0af 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -329,6 +329,18 @@ export const alertRuleApi = alertingApi.injectEndpoints({ invalidatesTags: (result, _error, { namespace, payload, rulerConfig }) => { const grafanaRulerRules = payload.rules.filter(rulerRuleType.grafana.rule); + const promTags: Array<{ type: 'GrafanaPrometheusGroups'; id: string }> = []; + if (rulerConfig.dataSourceUid === GRAFANA_RULES_SOURCE_NAME) { + promTags.push( + { type: 'GrafanaPrometheusGroups', id: `grafana/${namespace}/__any__/` }, + { type: 'GrafanaPrometheusGroups', id: `grafana/${namespace}/${payload.name}/__any__` }, + ...grafanaRulerRules.map((rule) => ({ + type: 'GrafanaPrometheusGroups' as const, + id: `grafana/${namespace}/${payload.name}/${rule.grafana_alert.title}`, + })) + ); + } + return [ { type: 'RuleNamespace', id: `${rulerConfig.dataSourceUid}/${namespace}` }, { type: 'RuleGroup', id: `${rulerConfig.dataSourceUid}/${namespace}/${payload.name}` }, @@ -336,6 +348,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({ { type: 'GrafanaRulerRule', id: rule.grafana_alert.uid } as const, { type: 'GrafanaRulerRuleVersion', id: rule.grafana_alert.uid } as const, ]), + ...promTags, 'DeletedRules', ]; }, diff --git a/public/app/features/alerting/unified/api/alertingApi.ts b/public/app/features/alerting/unified/api/alertingApi.ts index e3c28894ea5..8c6852fb07a 100644 --- a/public/app/features/alerting/unified/api/alertingApi.ts +++ b/public/app/features/alerting/unified/api/alertingApi.ts @@ -130,6 +130,7 @@ export const alertingApi = createApi({ 'ContactPointsStatus', 'Receiver', 'DeletedRules', + 'GrafanaPrometheusGroups', ], endpoints: () => ({}), }); diff --git a/public/app/features/alerting/unified/api/prometheusApi.ts b/public/app/features/alerting/unified/api/prometheusApi.ts index 24641f6c4e6..3c2a8b50efe 100644 --- a/public/app/features/alerting/unified/api/prometheusApi.ts +++ b/public/app/features/alerting/unified/api/prometheusApi.ts @@ -1,3 +1,6 @@ +import { useCallback } from 'react'; + +import { useDispatch } from 'app/types'; import { GrafanaPromRuleGroupDTO, PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; @@ -79,6 +82,31 @@ export const prometheusApi = alertingApi.injectEndpoints({ group_next_token: groupNextToken, }, }), + providesTags: (_result, _error, { folderUid, groupName, ruleName }) => { + const folderKey = folderUid ?? '__any__'; + const groupKey = groupName ?? '__any__'; + const ruleKey = ruleName ?? '__any__'; + return [{ type: 'GrafanaPrometheusGroups', id: `grafana/${folderKey}/${groupKey}/${ruleKey}` }]; + }, }), }), }); + +export function usePopulateGrafanaPrometheusApiCache() { + const dispatch = useDispatch(); + + const populateGroupResponseCache = useCallback( + (group: GrafanaPromRuleGroupDTO) => { + dispatch( + prometheusApi.util.upsertQueryData( + 'getGrafanaGroups', + { folderUid: group.folderUid, groupName: group.name }, + { data: { groups: [group] }, status: 'success' } + ) + ); + }, + [dispatch] + ); + + return { populateGroupResponseCache }; +} diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx index 87194f42eb9..67a0b30542a 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx @@ -18,7 +18,6 @@ import { trackRulesSearchComponentInteraction, trackRulesSearchInputInteraction, } from '../../../Analytics'; -import { shouldUseAlertingListViewV2 } from '../../../featureToggles'; import { useRulesFilter } from '../../../hooks/useFilteredRules'; import { useAlertingHomePageExtensions } from '../../../plugins/useAlertingHomePageExtensions'; import { RuleHealth } from '../../../search/rulesSearchParser'; @@ -43,10 +42,8 @@ const RuleHealthOptions: SelectableValue[] = [ // Contact point selector is not supported in Alerting ListView V2 yet const canRenderContactPointSelector = - (contextSrv.hasPermission(AccessControlAction.AlertingReceiversRead) && - config.featureToggles.alertingSimplifiedRouting && - shouldUseAlertingListViewV2() === false) ?? - false; + contextSrv.hasPermission(AccessControlAction.AlertingReceiversRead) && + config.featureToggles.alertingSimplifiedRouting; interface RulesFilerProps { onClear?: () => void; diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 7c8e5374a5d..de3b06d7a08 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -231,6 +231,7 @@ export const mockGrafanaPromAlertingRule = ( ...mockPromAlertingRule(), uid: 'mock-rule-uid-123', folderUid: 'NAMESPACE_UID', + isPaused: false, ...partial, }; }; diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 1e7253457b9..f83523e5a71 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -158,7 +158,7 @@ function FilterViewResults({ filterState }: FilterViewProps) { return ( diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx index 565c5fb4884..ef76569b713 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx @@ -164,6 +164,7 @@ function rulerRuleToPromRule(rule: RulerGrafanaRuleDTO): GrafanaPromRuleDTO { query: JSON.stringify(rule.grafana_alert.data), uid: rule.grafana_alert.uid, folderUid: rule.grafana_alert.namespace_uid, + isPaused: false, health: 'ok', state: PromAlertingRuleState.Inactive, type: rulerRuleType.grafana.alertingRule(rule) ? PromRuleType.Alerting : PromRuleType.Recording, diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx index df87f6fcd15..bf7ea3dd702 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx @@ -5,6 +5,7 @@ import { t } from 'app/core/internationalization'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; import { GrafanaPromRuleDTO, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; +import { logWarning } from '../Analytics'; import { alertRuleApi } from '../api/alertRuleApi'; import { prometheusApi } from '../api/prometheusApi'; import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; @@ -154,5 +155,13 @@ export function matchRules( matchingResult.promOnlyRules.push(...promRulesMap.values()); + if (matchingResult.promOnlyRules.length > 0) { + // Grafana Prometheus rules should be strongly consistent now so each Ruler rule should have a matching Prometheus rule + // If not, log it as a warning + logWarning('Grafana Managed Rules: No matching Prometheus rule found for Ruler rule', { + promOnlyRulesCount: matchingResult.promOnlyRules.length.toString(), + }); + } + return matchingResult; } diff --git a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx index 5cb1b7ebf83..08194fc65a4 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx @@ -1,9 +1,10 @@ import { Alert } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; -import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; +import { GrafanaRuleGroupIdentifier, GrafanaRuleIdentifier } from 'app/types/unified-alerting'; import { GrafanaPromRuleDTO, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; import { alertRuleApi } from '../api/alertRuleApi'; +import { prometheusApi } from '../api/prometheusApi'; import { GrafanaRulesSource } from '../utils/datasource'; import { rulerRuleType } from '../utils/rules'; import { createRelativeUrl } from '../utils/url'; @@ -19,44 +20,55 @@ import { RuleActionsButtons } from './components/RuleActionsButtons.V2'; import { RuleOperation } from './components/RuleListIcon'; const { useGetGrafanaRulerGroupQuery } = alertRuleApi; +const { useGetGrafanaGroupsQuery } = prometheusApi; interface GrafanaRuleLoaderProps { - rule: GrafanaPromRuleDTO; - + ruleIdentifier: GrafanaRuleIdentifier; groupIdentifier: GrafanaRuleGroupIdentifier; namespaceName: string; } -export function GrafanaRuleLoader({ rule, groupIdentifier, namespaceName }: GrafanaRuleLoaderProps) { +export function GrafanaRuleLoader({ ruleIdentifier, groupIdentifier, namespaceName }: GrafanaRuleLoaderProps) { const { data: rulerRuleGroup, - isError, - isLoading, + error: rulerRuleGroupError, + isLoading: isRulerRuleGroupLoading, } = useGetGrafanaRulerGroupQuery({ folderUid: groupIdentifier.namespace.uid, groupName: groupIdentifier.groupName, }); + const { + data: promRuleGroup, + error: promRuleGroupError, + isLoading: isPromRuleGroupLoading, + } = useGetGrafanaGroupsQuery({ + folderUid: groupIdentifier.namespace.uid, + groupName: groupIdentifier.groupName, + }); - const rulerRule = rulerRuleGroup?.rules.find((rulerRule) => rulerRule.grafana_alert.uid === rule.uid); + const rulerRule = rulerRuleGroup?.rules.find((rulerRule) => rulerRule.grafana_alert.uid === ruleIdentifier.uid); + const promRule = promRuleGroup?.data.groups + .flatMap((group) => group.rules) + .find((promRule) => promRule.uid === ruleIdentifier.uid); - if (isError) { - return ; + if (rulerRuleGroupError || promRuleGroupError) { + return ; } - if (isLoading) { + if (isRulerRuleGroupLoading || isPromRuleGroupLoading) { return ; } if (!rulerRule) { return ( - Cannot find rule details for {{ uid: rule.uid ?? '' }} + Cannot find rule details for UID {{ uid: ruleIdentifier.uid ?? '' }} ); @@ -64,7 +76,7 @@ export function GrafanaRuleLoader({ rule, groupIdentifier, namespaceName }: Graf return ( , }; diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx index c74bd088ca6..e5a179231bc 100644 --- a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx @@ -1,7 +1,9 @@ import Skeleton from 'react-loading-skeleton'; import { t } from 'app/core/internationalization'; -import { PromRuleDTO } from 'app/types/unified-alerting-dto'; +import { GrafanaRuleIdentifier } from 'app/types/unified-alerting'; + +import { stringifyErrorLike } from '../../utils/misc'; import { ListItem } from './ListItem'; import { RuleActionsSkeleton } from './RuleActionsSkeleton'; @@ -19,12 +21,16 @@ export function AlertRuleListItemSkeleton() { ); } -export function RulerRuleLoadingError({ rule }: { rule: PromRuleDTO }) { - return ( - - ); +export function RulerRuleLoadingError({ + ruleIdentifier, + error, +}: { + ruleIdentifier: GrafanaRuleIdentifier; + error?: unknown; +}) { + const errorMessage = error + ? stringifyErrorLike(error) + : t('alerting.rule-list.rulerrule-loading-error', 'Failed to load the rule'); + + return ; } diff --git a/public/app/features/alerting/unified/rule-list/hooks/filters.ts b/public/app/features/alerting/unified/rule-list/hooks/filters.ts index 89eb4c96f6b..e66ddb559a9 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/filters.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/filters.ts @@ -85,6 +85,20 @@ export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter) { return false; } + if (filterState.contactPoint) { + if (!prometheusRuleType.grafana.alertingRule(rule)) { + return false; + } + + if (!rule.notificationSettings) { + return false; + } + + if (filterState.contactPoint !== rule.notificationSettings.receiver) { + return false; + } + } + // Dashboard UID filter if (filterState.dashboardUid) { if (!prometheusRuleType.alertingRule(rule)) { diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index 50be4d9054d..0b7fc4159a6 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -15,6 +15,7 @@ import { PromRuleGroupDTO, } from 'app/types/unified-alerting-dto'; +import { usePopulateGrafanaPrometheusApiCache } from '../../api/prometheusApi'; import { RulesFilter } from '../../search/rulesSearchParser'; import { getDataSourceByUid, @@ -51,6 +52,7 @@ interface GetIteratorResult { } export function useFilteredRulesIteratorProvider() { + const { populateGroupResponseCache } = usePopulateGrafanaPrometheusApiCache(); const allExternalRulesSources = getExternalRulesSources(); const prometheusGroupsGenerator = usePrometheusGroupsGenerator(); @@ -68,7 +70,10 @@ export function useFilteredRulesIteratorProvider() { concatMap((groups) => groups .filter((group) => groupFilter(group, normalizedFilterState)) - .flatMap((group) => group.rules.map((rule) => [group, rule] as const)) + .flatMap((group) => { + populateGroupResponseCache(group); + return group.rules.map((rule) => [group, rule] as const); + }) .filter(([, rule]) => ruleFilter(rule, normalizedFilterState)) .map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule)) ), diff --git a/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts b/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts index be447f4fa6c..1363b10ad9a 100644 --- a/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts @@ -80,6 +80,7 @@ export function getPrometheusRulesResponse( query: '[{"refId":"A","queryType":"","relativeTimeRange":{"from":600,"to":0},"datasourceUid":"gdev-testdata","model":{"hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A"}},{"refId":"B","queryType":"","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"__expr__","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"name":"Expression","type":"__expr__","uid":"__expr__"},"expression":"A","intervalMs":1000,"maxDataPoints":43200,"refId":"B","type":"threshold"}}]', duration: 300, + isPaused: false, health: 'ok', type: PromRuleType.Alerting, lastEvaluation: '0001-01-01T00:00:00Z', diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index c96d35b3736..b3d66445587 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -129,6 +129,7 @@ interface PromRuleDTOBase { interface GrafanaPromRuleDTOBase extends PromRuleDTOBase { uid: string; folderUid: string; + isPaused: boolean; queriedDatasourceUIDs?: string[]; } @@ -145,6 +146,7 @@ export interface PromAlertingRuleDTO extends PromRuleDTOBase { duration?: number; // for state: PromAlertingRuleState; type: PromRuleType.Alerting; + notificationSettings?: GrafanaNotificationSettings; } export interface PromRecordingRuleDTO extends PromRuleDTOBase { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2e2bd544668..68200392e56 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2026,8 +2026,8 @@ "title-inspect-alert-rule": "Inspect Alert rule" }, "rule-list": { - "cannot-find-rule-details-for": "Cannot find rule details for {{uid}}", - "cannot-load-rule-details-for": "Cannot load rule details for {{name}}", + "cannot-find-rule-details-for": "Cannot find rule details for UID {{uid}}", + "cannot-load-rule-details-for": "Cannot load rule details for UID {{uid}}", "configure-datasource": "Configure", "draft-new-rule": "Draft a new rule", "ds-error-boundary": {