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 15 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
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