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
pull/104958/head
Konrad Lalik 2 months ago committed by GitHub
parent f2b9830fda
commit f79f6efdcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      public/app/features/alerting/unified/api/alertRuleApi.ts
  2. 1
      public/app/features/alerting/unified/api/alertingApi.ts
  3. 28
      public/app/features/alerting/unified/api/prometheusApi.ts
  4. 7
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx
  5. 1
      public/app/features/alerting/unified/mocks.ts
  6. 2
      public/app/features/alerting/unified/rule-list/FilterView.tsx
  7. 1
      public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx
  8. 9
      public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx
  9. 42
      public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx
  10. 24
      public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx
  11. 14
      public/app/features/alerting/unified/rule-list/hooks/filters.ts
  12. 7
      public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts
  13. 1
      public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts
  14. 2
      public/app/types/unified-alerting-dto.ts
  15. 4
      public/locales/en-US/grafana.json

@ -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',
];
},

@ -130,6 +130,7 @@ export const alertingApi = createApi({
'ContactPointsStatus',
'Receiver',
'DeletedRules',
'GrafanaPrometheusGroups',
],
endpoints: () => ({}),
});

@ -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 };
}

@ -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;

@ -231,6 +231,7 @@ export const mockGrafanaPromAlertingRule = (
...mockPromAlertingRule(),
uid: 'mock-rule-uid-123',
folderUid: 'NAMESPACE_UID',
isPaused: false,
...partial,
};
};

@ -158,7 +158,7 @@ function FilterViewResults({ filterState }: FilterViewProps) {
return (
<GrafanaRuleLoader
key={key}
rule={rule}
ruleIdentifier={{ ruleSourceName: 'grafana', uid: rule.uid }}
groupIdentifier={groupIdentifier}
namespaceName={ruleWithOrigin.namespaceName}
/>

@ -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,

@ -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;
}

@ -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 <RulerRuleLoadingError rule={rule} />;
if (rulerRuleGroupError || promRuleGroupError) {
return <RulerRuleLoadingError ruleIdentifier={ruleIdentifier} error={rulerRuleGroupError || promRuleGroupError} />;
}
if (isLoading) {
if (isRulerRuleGroupLoading || isPromRuleGroupLoading) {
return <AlertRuleListItemSkeleton />;
}
if (!rulerRule) {
return (
<Alert
title={t('alerting.rule-list.cannot-load-rule-details-for', 'Cannot load rule details for {{name}}', {
name: rule.name,
title={t('alerting.rule-list.cannot-load-rule-details-for', 'Cannot load rule details for UID {{uid}}', {
uid: ruleIdentifier.uid,
})}
severity="error"
>
<Trans i18nKey="alerting.rule-list.cannot-find-rule-details-for">
Cannot find rule details for {{ uid: rule.uid ?? '<empty uid>' }}
Cannot find rule details for UID {{ uid: ruleIdentifier.uid ?? '<empty uid>' }}
</Trans>
</Alert>
);
@ -64,7 +76,7 @@ export function GrafanaRuleLoader({ rule, groupIdentifier, namespaceName }: Graf
return (
<GrafanaRuleListItem
rule={rule}
rule={promRule}
rulerRule={rulerRule}
groupIdentifier={groupIdentifier}
namespaceName={namespaceName}
@ -103,7 +115,7 @@ export function GrafanaRuleListItem({
error: rule?.lastError,
labels: labels,
isProvisioned: Boolean(provenance),
isPaused: is_paused,
isPaused: rule?.isPaused ?? is_paused,
application: 'grafana' as const,
actions: <RuleActionsButtons rule={rulerRule} promRule={rule} groupIdentifier={groupIdentifier} compact />,
};

@ -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 (
<ListItem
title={rule.name}
description={t('alerting.rule-list.rulerrule-loading-error', 'Failed to load the rule')}
data-testid="ruler-rule-loading-error"
/>
);
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 <ListItem title={ruleIdentifier.uid} description={errorMessage} data-testid="ruler-rule-loading-error" />;
}

@ -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)) {

@ -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))
),

@ -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',

@ -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 {

@ -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": {

Loading…
Cancel
Save