Skip to content

Commit

Permalink
Alerting: Reduce number of request fetching rules in the dashboard vi…
Browse files Browse the repository at this point in the history
…ew using rtkq (#86991)

* Reduce number of request fetching rules in the dashboard view using rtkq

* Fix UnifiedAlertStatesWorker work

* refactor ungroupRulesByFileName

* Address review comments and fix test

* fix DashboardQueryRunner test

* Fix tests

* Update AlertStatesDataLayer to use RTKQ

* Fix PanelAlertTabContent test

* Fix PanelAlertTabContent test after adding RTKQ

* fix test and address PR review comments

* Update useCombinedRuleNamespaces to have both dashboardUID and panelId as optional params and rename the hook

* Address review pr comment

* remove test about template variables

* Use poll interval in useCombinedRules
  • Loading branch information
soniaAguilarPeiron committed May 10, 2024
1 parent d9d25dc commit 4b72020
Show file tree
Hide file tree
Showing 15 changed files with 537 additions and 467 deletions.
189 changes: 87 additions & 102 deletions public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
@@ -1,45 +1,44 @@
import { render, waitFor } from '@testing-library/react';
import { render } from '@testing-library/react';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { byTestId } from 'testing-library-selector';

import { DataSourceApi } from '@grafana/data';
import { PromOptions, PrometheusDatasource } from '@grafana/prometheus';
import { setDataSourceSrv } from '@grafana/runtime';
import { setDataSourceSrv, setPluginExtensionsHook } from '@grafana/runtime';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { toggleOption } from 'app/features/variables/pickers/OptionsPicker/reducer';
import { toKeyedAction } from 'app/features/variables/state/keyedVariablesReducer';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
import { AlertQuery } from 'app/types/unified-alerting-dto';
import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto';

import { PanelAlertTabContent } from './PanelAlertTabContent';
import { fetchRules } from './api/prometheus';
import { fetchRulerRules } from './api/ruler';
import * as apiRuler from './api/ruler';
import * as alertingAbilities from './hooks/useAbilities';
import { mockAlertRuleApi, setupMswServer } from './mockApi';
import {
MockDataSourceSrv,
grantUserPermissions,
mockDataSource,
MockDataSourceSrv,
mockPromAlert,
mockPromAlertingRule,
mockPromRuleGroup,
mockPromRuleNamespace,
mockRulerGrafanaRule,
mockRulerAlertingRule,
mockRulerRuleGroup,
} from './mocks';
import { RuleFormValues } from './types/rule-form';
import * as config from './utils/config';
import { Annotation } from './utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import * as ruleFormUtils from './utils/rule-form';

jest.mock('./api/prometheus');
jest.mock('./api/ruler');
jest.mock('../../../core/hooks/useMediaQueryChange');

jest.spyOn(config, 'getAllDataSources');
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);

jest.spyOn(apiRuler, 'rulerUrlBuilder');
jest.spyOn(alertingAbilities, 'useAlertRuleAbility');
const dataSources = {
prometheus: mockDataSource<PromOptions>({
name: 'Prometheus',
Expand All @@ -57,10 +56,8 @@ dataSources.default.meta.alerting = true;

const mocks = {
getAllDataSources: jest.mocked(config.getAllDataSources),
api: {
fetchRules: jest.mocked(fetchRules),
fetchRulerRules: jest.mocked(fetchRulerRules),
},
useAlertRuleAbilityMock: jest.mocked(alertingAbilities.useAlertRuleAbility),
rulerBuilderMock: jest.mocked(apiRuler.rulerUrlBuilder),
};

const renderAlertTabContent = (
Expand All @@ -75,70 +72,82 @@ const renderAlertTabContent = (
);
};

const rules = [
mockPromRuleNamespace({
name: 'default',
const promResponse: PromRulesResponse = {
status: 'success',
data: {
groups: [
mockPromRuleGroup({
{
name: 'mygroup',
file: 'default',
rules: [
mockPromAlertingRule({
name: 'dashboardrule1',
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
alerts: [
mockPromAlert({
labels: { severity: 'critical' },
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
}),
],
totals: { alerting: 1 },
totalsFiltered: { alerting: 1 },
}),
],
}),
mockPromRuleGroup({
interval: 20,
},
{
name: 'othergroup',
file: 'default',
rules: [
mockPromAlertingRule({
name: 'dashboardrule2',
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
alerts: [
mockPromAlert({
labels: { severity: 'critical' },
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
}),
],
totals: { alerting: 1 },
totalsFiltered: { alerting: 1 },
}),
],
}),
interval: 20,
},
],
}),
];

const rulerRules = {
totals: {
alerting: 2,
},
},
};
const rulerResponse = {
default: [
{
mockRulerRuleGroup({
name: 'mygroup',
rules: [
mockRulerGrafanaRule(
{
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
mockRulerAlertingRule({
alert: 'dashboardrule1',
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
{
title: 'dashboardrule1',
}
),
}),
],
},
}),
{
name: 'othergroup',
rules: [
mockRulerGrafanaRule(
{
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
mockRulerAlertingRule({
alert: 'dashboardrule2',
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
{
title: 'dashboardrule2',
}
),
}),
],
},
],
Expand Down Expand Up @@ -177,6 +186,8 @@ const ui = {
createButton: byTestId<HTMLAnchorElement>('create-alert-rule-button'),
};

const server = setupMswServer();

describe('PanelAlertTabContent', () => {
beforeEach(() => {
jest.resetAllMocks();
Expand All @@ -188,13 +199,28 @@ describe('PanelAlertTabContent', () => {
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);

setPluginExtensionsHook(() => ({
extensions: [],
isLoading: false,
}));

mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
const dsService = new MockDataSourceSrv(dataSources);
dsService.datasources[dataSources.prometheus.uid] = new PrometheusDatasource(
dataSources.prometheus
) as DataSourceApi;
dsService.datasources[dataSources.default.uid] = new PrometheusDatasource(dataSources.default) as DataSourceApi;
setDataSourceSrv(dsService);
mocks.rulerBuilderMock.mockReturnValue({
rules: () => ({ path: `api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules` }),
namespace: () => ({ path: 'ruler' }),
namespaceGroup: () => ({ path: 'ruler' }),
});
mocks.useAlertRuleAbilityMock.mockReturnValue([true, true]);

mockAlertRuleApi(server).prometheusRuleNamespaces(GRAFANA_RULES_SOURCE_NAME, promResponse);
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, rulerResponse);
});

it('Will take into account panel maxDataPoints', async () => {
Expand Down Expand Up @@ -286,14 +312,12 @@ describe('PanelAlertTabContent', () => {
});
});

it.skip('Will render alerts belonging to panel and a button to create alert from panel queries', async () => {
mocks.api.fetchRules.mockResolvedValue(rules);
mocks.api.fetchRulerRules.mockResolvedValue(rulerRules);

it('Will render alerts belonging to panel and a button to create alert from panel queries', async () => {
renderAlertTabContent(dashboard, panel);

const rows = await ui.row.findAll();
expect(rows).toHaveLength(1);
// after updating to RTKQ, the response is already returning the alerts belonging to the panel
expect(rows).toHaveLength(2);
expect(rows[0]).toHaveTextContent(/dashboardrule1/);
expect(rows[0]).not.toHaveTextContent(/dashboardrule2/);

Expand All @@ -315,44 +339,5 @@ describe('PanelAlertTabContent', () => {
};

expect(defaultsWithDeterministicTime).toMatchSnapshot();

expect(mocks.api.fetchRulerRules).toHaveBeenCalledWith(
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
{
dashboardUID: dashboard.uid,
panelId: panel.id,
}
);
expect(mocks.api.fetchRules).toHaveBeenCalledWith(
GRAFANA_RULES_SOURCE_NAME,
{
dashboardUID: dashboard.uid,
panelId: panel.id,
},
undefined,
undefined,
undefined,
undefined
);
});

it('Update NewRuleFromPanel button url when template changes', async () => {
const panelToRuleValuesSpy = jest.spyOn(ruleFormUtils, 'panelToRuleFormValues');

const store = configureStore();
renderAlertTabContent(dashboard, panel, store);

store.dispatch(
toKeyedAction(
'optionKey',
toggleOption({
option: { value: 'optionValue', selected: true, text: 'Option' },
clearOthers: false,
forceSelect: false,
})
)
);

await waitFor(() => expect(panelToRuleValuesSpy).toHaveBeenCalledTimes(2));
});
});
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
17 changes: 15 additions & 2 deletions public/app/features/alerting/unified/api/prometheus.ts
Expand Up @@ -2,14 +2,14 @@ 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';
import { isCloudRuleIdentifier, isPrometheusRuleIdentifier } from '../utils/rules';

export interface FetchPromRulesFilter {
dashboardUID: string;
dashboardUID?: string;
panelId?: number;
}

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
2 changes: 1 addition & 1 deletion public/app/features/alerting/unified/api/ruler.ts
Expand Up @@ -68,7 +68,7 @@ export async function setRulerRuleGroup(
}

export interface FetchRulerRulesFilter {
dashboardUID: string;
dashboardUID?: string;
panelId?: number;
}

Expand Down
Expand Up @@ -282,7 +282,7 @@ export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdenti
};
}

const grafanaRulerConfig: RulerDataSourceConfig = {
export const grafanaRulerConfig: RulerDataSourceConfig = {
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
apiVersion: 'legacy',
};
Expand Down

0 comments on commit 4b72020

Please sign in to comment.