Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alerting: Reduce number of request fetching rules in the dashboard view using rtkq #86991

Merged
merged 17 commits into from May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -11,6 +11,7 @@ import { NewRuleFromPanelButton } from './components/panel-alerts-tab/NewRuleFro
import { RulesTable } from './components/rules/RulesTable';
import { usePanelCombinedRules } from './hooks/usePanelCombinedRules';
import { getRulesPermissions } from './utils/access-control';
import { stringifyErrorLike } from './utils/misc';

interface Props {
dashboard: DashboardModel;
Expand All @@ -30,7 +31,7 @@ export const PanelAlertTabContent = ({ dashboard, panel }: Props) => {
const alert = errors.length ? (
<Alert title="Errors loading rules" severity="error">
{errors.map((error, index) => (
<div key={index}>Failed to load Grafana rules state: {error.message || 'Unknown error.'}</div>
<div key={index}>Failed to load Grafana rules state: {stringifyErrorLike(error)}</div>
))}
</Alert>
) : null;
Expand Down
12 changes: 10 additions & 2 deletions public/app/features/alerting/unified/api/alertRuleApi.ts
Expand Up @@ -162,14 +162,22 @@ export const alertRuleApi = alertingApi.injectEndpoints({

prometheusRuleNamespaces: build.query<
RuleNamespace[],
{ ruleSourceName: string; namespace?: string; groupName?: string; ruleName?: string; dashboardUid?: string }
{
ruleSourceName: string;
namespace?: string;
groupName?: string;
ruleName?: string;
dashboardUid?: string;
panelId?: number;
}
>({
query: ({ ruleSourceName, namespace, groupName, ruleName, dashboardUid }) => {
query: ({ ruleSourceName, namespace, groupName, ruleName, dashboardUid, panelId }) => {
const queryParams: Record<string, string | undefined> = {
file: namespace,
rule_group: groupName,
rule_name: ruleName,
dashboard_uid: dashboardUid, // Supported only by Grafana managed rules
panel_id: panelId?.toString(), // Supported only by Grafana managed rules
};

return {
Expand Down
15 changes: 14 additions & 1 deletion public/app/features/alerting/unified/api/prometheus.ts
Expand Up @@ -2,7 +2,7 @@ import { lastValueFrom } from 'rxjs';

import { getBackendSrv } from '@grafana/runtime';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { RuleIdentifier, RuleNamespace } from 'app/types/unified-alerting';
import { RuleGroup, RuleIdentifier, RuleNamespace } from 'app/types/unified-alerting';
import { PromRuleGroupDTO, PromRulesResponse } from 'app/types/unified-alerting-dto';

import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
Expand Down Expand Up @@ -102,7 +102,20 @@ export const groupRulesByFileName = (groups: PromRuleGroupDTO[], dataSourceName:

return Object.values(nsMap);
};
export const ungroupRulesByFileName = (namespaces: RuleNamespace[] = []): PromRuleGroupDTO[] => {
return namespaces?.flatMap((namespace) =>
namespace.groups.flatMap((group) => ruleGroupToPromRuleGroupDTO(group, namespace.name))
);
};

function ruleGroupToPromRuleGroupDTO(group: RuleGroup, namespace: string): PromRuleGroupDTO {
return {
name: group.name,
file: namespace,
rules: group.rules,
interval: group.interval,
};
}
export async function fetchRules(
dataSourceName: string,
filter?: FetchPromRulesFilter,
Expand Down
110 changes: 107 additions & 3 deletions public/app/features/alerting/unified/hooks/useCombinedRule.ts
@@ -1,25 +1,40 @@
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { useAsync } from 'react-use';

import { useDispatch } from 'app/types';
import { CombinedRule, RuleIdentifier, RuleNamespace, RulerDataSourceConfig } from 'app/types/unified-alerting';
import {
CombinedRule,
CombinedRuleNamespace,
RuleIdentifier,
RuleNamespace,
RulerDataSourceConfig,
} from 'app/types/unified-alerting';
import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';

import { alertRuleApi } from '../api/alertRuleApi';
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
import { fetchPromAndRulerRulesAction } from '../state/actions';
import { getDataSourceByName, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../utils/datasource';
import {
getDataSourceByName,
getRulesSourceByName,
GRAFANA_RULES_SOURCE_NAME,
isGrafanaRulesSource,
} from '../utils/datasource';
import { AsyncRequestMapSlice, AsyncRequestState, initialAsyncRequestState } from '../utils/redux';
import * as ruleId from '../utils/rule-id';
import {
isCloudRuleIdentifier,
isGrafanaRuleIdentifier,
isGrafanaRulerRule,
isPrometheusRuleIdentifier,
isRulerNotSupportedResponse,
} from '../utils/rules';

import {
addPromGroupsToCombinedNamespace,
addRulerGroupsToCombinedNamespace,
attachRulerRulesToCombinedRules,
CacheValue,
combineRulesNamespaces,
useCombinedRuleNamespaces,
} from './useCombinedRuleNamespaces';
Expand Down Expand Up @@ -162,6 +177,95 @@ function getRequestState(
return state;
}

/*
This hook returns combined Grafana rules by dashpboard UID
*/
export function useCombinedRulesByDashboard(
dashboardUID: string,
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
panelId?: number
): {
loading: boolean;
result?: CombinedRuleNamespace[];
error?: unknown;
} {
const {
currentData: promRuleNs,
isLoading: isLoadingPromRules,
error: promRuleNsError,
} = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery({
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
dashboardUid: dashboardUID,
panelId,
});

const {
currentData: rulerRules,
isLoading: isLoadingRulerRules,
error: rulerRulesError,
} = alertRuleApi.endpoints.rulerRules.useQuery({
rulerConfig: grafanaRulerConfig,
filter: { dashboardUID: dashboardUID, panelId },
});
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved

//---------
// cache results per rules source, so we only recalculate those for which results have actually changed
const cache = useRef<Record<string, CacheValue>>({});

gillesdemey marked this conversation as resolved.
Show resolved Hide resolved
const rulesSource = getRulesSourceByName(GRAFANA_RULES_SOURCE_NAME);

const rules = useMemo(() => {
if (!rulesSource) {
return [];
}

const cached = cache.current[GRAFANA_RULES_SOURCE_NAME];
if (cached && cached.promRules === promRuleNs && cached.rulerRules === rulerRules) {
return cached.result;
}
const namespaces: Record<string, CombinedRuleNamespace> = {};

// first get all the ruler rules from the data source
Object.entries(rulerRules || {}).forEach(([namespaceName, groups]) => {
const namespace: CombinedRuleNamespace = {
rulesSource,
name: namespaceName,
groups: [],
};

// We need to set the namespace_uid for grafana rules as it's required to obtain the rule's groups
// All rules from all groups have the same namespace_uid so we're taking the first one.
if (isGrafanaRulerRule(groups[0].rules[0])) {
namespace.uid = groups[0].rules[0].grafana_alert.namespace_uid;
}

namespaces[namespaceName] = namespace;
addRulerGroupsToCombinedNamespace(namespace, groups);
});

// then correlate with prometheus rules
promRuleNs?.forEach(({ name: namespaceName, groups }) => {
const ns = (namespaces[namespaceName] = namespaces[namespaceName] || {
rulesSource,
name: namespaceName,
groups: [],
});

addPromGroupsToCombinedNamespace(ns, groups);
});

const result = Object.values(namespaces);

cache.current[GRAFANA_RULES_SOURCE_NAME] = { promRules: promRuleNs, rulerRules, result };
return result;
}, [promRuleNs, rulerRules, rulesSource]);

return {
loading: isLoadingPromRules || isLoadingRulerRules,
error: promRuleNsError ?? rulerRulesError,
result: rules,
};
}

export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }): {
loading: boolean;
result?: CombinedRule;
Expand Down
Expand Up @@ -38,7 +38,7 @@ import {

import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';

interface CacheValue {
export interface CacheValue {
promRules?: RuleNamespace[];
rulerRules?: RulerRulesConfigDTO | null;
result: CombinedRuleNamespace[];
Expand Down Expand Up @@ -211,7 +211,10 @@ export function sortRulesByName(rules: CombinedRule[]) {
return rules.sort((a, b) => a.name.localeCompare(b.name));
}

function addRulerGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RulerRuleGroupDTO[] = []): void {
export function addRulerGroupsToCombinedNamespace(
namespace: CombinedRuleNamespace,
groups: RulerRuleGroupDTO[] = []
): void {
namespace.groups = groups.map((group) => {
const numRecordingRules = group.rules.filter((rule) => isRecordingRulerRule(rule)).length;
const numPaused = group.rules.filter((rule) => isGrafanaRulerRule(rule) && rule.grafana_alert.is_paused).length;
Expand All @@ -231,7 +234,7 @@ function addRulerGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, gro
});
}

function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RuleGroup[]): void {
export function addPromGroupsToCombinedNamespace(namespace: CombinedRuleNamespace, groups: RuleGroup[]): void {
const existingGroupsByName = new Map<string, CombinedRuleGroup>();
namespace.groups.forEach((group) => existingGroupsByName.set(group.name, group));

Expand Down
@@ -1,16 +1,6 @@
import { SerializedError } from '@reduxjs/toolkit';
import { useEffect, useMemo } from 'react';

import { useDispatch } from 'app/types';
import { CombinedRule } from 'app/types/unified-alerting';

import { fetchPromRulesAction, fetchRulerRulesAction } from '../state/actions';
import { Annotation, RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { initialAsyncRequestState } from '../utils/redux';

import { useCombinedRuleNamespaces } from './useCombinedRuleNamespaces';
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
import { useCombinedRulesByDashboard } from './useCombinedRule';

interface Options {
dashboardUID: string;
Expand All @@ -20,70 +10,19 @@ interface Options {
}

interface ReturnBag {
errors: SerializedError[];
errors: unknown[];
rules: CombinedRule[];

loading?: boolean;
}

export function usePanelCombinedRules({ dashboardUID, panelId, poll = false }: Options): ReturnBag {
const dispatch = useDispatch();

const promRuleRequest =
useUnifiedAlertingSelector((state) => state.promRules[GRAFANA_RULES_SOURCE_NAME]) ?? initialAsyncRequestState;
const rulerRuleRequest =
useUnifiedAlertingSelector((state) => state.rulerRules[GRAFANA_RULES_SOURCE_NAME]) ?? initialAsyncRequestState;

// fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS
useEffect(() => {
const fetch = () => {
dispatch(
fetchPromRulesAction({
rulesSourceName: GRAFANA_RULES_SOURCE_NAME,
filter: { dashboardUID, panelId },
})
);
dispatch(
fetchRulerRulesAction({
rulesSourceName: GRAFANA_RULES_SOURCE_NAME,
filter: { dashboardUID, panelId },
})
);
};
fetch();
if (poll) {
const interval = setInterval(fetch, RULE_LIST_POLL_INTERVAL_MS);
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
return () => {
clearInterval(interval);
};
}
return () => {};
}, [dispatch, poll, panelId, dashboardUID]);

const loading = promRuleRequest.loading || rulerRuleRequest.loading;
const errors = [promRuleRequest.error, rulerRuleRequest.error].filter(
(err: SerializedError | undefined): err is SerializedError => !!err
);

const combinedNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);

// filter out rules that are relevant to this panel
const rules = useMemo(
(): CombinedRule[] =>
combinedNamespaces
.flatMap((ns) => ns.groups)
.flatMap((group) => group.rules)
.filter(
(rule) =>
rule.annotations[Annotation.dashboardUID] === dashboardUID &&
rule.annotations[Annotation.panelID] === String(panelId)
),
[combinedNamespaces, dashboardUID, panelId]
);
const { result: combinedNamespaces, loading, error } = useCombinedRulesByDashboard(dashboardUID, panelId);
const rules = combinedNamespaces ? combinedNamespaces.flatMap((ns) => ns.groups).flatMap((group) => group.rules) : [];

return {
rules,
errors,
errors: error ? [error] : [],
loading,
};
}
@@ -1,33 +1,17 @@
import React from 'react';
import { useAsync } from 'react-use';

import { LoadingPlaceholder } from '@grafana/ui';
import { useDispatch } from 'app/types';

import { RulesTable } from '../components/rules/RulesTable';
import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces';
import { fetchPromAndRulerRulesAction } from '../state/actions';
import { Annotation } from '../utils/constants';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { useCombinedRulesByDashboard } from '../hooks/useCombinedRule';

interface Props {
dashboardUid: string;
}

export default function AlertRulesDrawerContent({ dashboardUid }: Props) {
const dispatch = useDispatch();

const { loading: loadingAlertRules } = useAsync(async () => {
await dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
}, [dispatch]);

const grafanaNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
const rules = grafanaNamespaces
.flatMap((ns) => ns.groups)
.flatMap((g) => g.rules)
.filter((rule) => rule.annotations[Annotation.dashboardUID] === dashboardUid);

const loading = loadingAlertRules;
const { loading, result: grafanaNamespaces } = useCombinedRulesByDashboard(dashboardUid);
const rules = grafanaNamespaces ? grafanaNamespaces.flatMap((ns) => ns.groups).flatMap((g) => g.rules) : [];

return (
<>
Expand Down