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
This commit is contained in:
@@ -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 />,
|
||||
};
|
||||
|
||||
+15
-9
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user