mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 18:23:50 +08:00
Alerting: Reduce number of request fetching rules in the dashboard view 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
This commit is contained in:
@ -1,37 +1,35 @@
|
||||
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');
|
||||
@ -39,7 +37,8 @@ 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',
|
||||
@ -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 = (
|
||||
@ -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',
|
||||
}
|
||||
),
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -177,6 +186,8 @@ const ui = {
|
||||
createButton: byTestId<HTMLAnchorElement>('create-alert-rule-button'),
|
||||
};
|
||||
|
||||
const server = setupMswServer();
|
||||
|
||||
describe('PanelAlertTabContent', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
@ -188,6 +199,12 @@ 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(
|
||||
@ -195,6 +212,15 @@ describe('PanelAlertTabContent', () => {
|
||||
) 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 () => {
|
||||
@ -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/);
|
||||
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
@ -68,7 +68,7 @@ export async function setRulerRuleGroup(
|
||||
}
|
||||
|
||||
export interface FetchRulerRulesFilter {
|
||||
dashboardUID: string;
|
||||
dashboardUID?: string;
|
||||
panelId?: number;
|
||||
}
|
||||
|
||||
|
@ -282,7 +282,7 @@ export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdenti
|
||||
};
|
||||
}
|
||||
|
||||
const grafanaRulerConfig: RulerDataSourceConfig = {
|
||||
export const grafanaRulerConfig: RulerDataSourceConfig = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
apiVersion: 'legacy',
|
||||
};
|
||||
|
@ -21,6 +21,8 @@ import {
|
||||
RulerRulesConfigDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
|
||||
import {
|
||||
getAllRulesSources,
|
||||
getRulesSourceByName,
|
||||
@ -36,9 +38,10 @@ import {
|
||||
isRecordingRulerRule,
|
||||
} from '../utils/rules';
|
||||
|
||||
import { grafanaRulerConfig } from './useCombinedRule';
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
|
||||
interface CacheValue {
|
||||
export interface CacheValue {
|
||||
promRules?: RuleNamespace[];
|
||||
rulerRules?: RulerRulesConfigDTO | null;
|
||||
result: CombinedRuleNamespace[];
|
||||
@ -211,7 +214,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;
|
||||
@ -231,7 +237,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));
|
||||
|
||||
@ -457,3 +463,103 @@ function hashQuery(query: string) {
|
||||
// labels matchers can be reordered, so sort the enitre string, esentially comparing just the character counts
|
||||
return query.split('').sort().join('');
|
||||
}
|
||||
|
||||
/*
|
||||
This hook returns combined Grafana rules. Optionally, it can filter rules by dashboard UID and panel ID.
|
||||
*/
|
||||
export function useCombinedRules(
|
||||
dashboardUID?: string,
|
||||
panelId?: number,
|
||||
poll?: boolean
|
||||
): {
|
||||
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,
|
||||
},
|
||||
{
|
||||
pollingInterval: poll ? RULE_LIST_POLL_INTERVAL_MS : undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
currentData: rulerRules,
|
||||
isLoading: isLoadingRulerRules,
|
||||
error: rulerRulesError,
|
||||
} = alertRuleApi.endpoints.rulerRules.useQuery(
|
||||
{
|
||||
rulerConfig: grafanaRulerConfig,
|
||||
filter: { dashboardUID, panelId },
|
||||
},
|
||||
{
|
||||
pollingInterval: poll ? RULE_LIST_POLL_INTERVAL_MS : undefined,
|
||||
}
|
||||
);
|
||||
|
||||
//---------
|
||||
// cache results per rules source, so we only recalculate those for which results have actually changed
|
||||
const cache = useRef<Record<string, CacheValue>>({});
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
@ -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 { useCombinedRules } from './useCombinedRuleNamespaces';
|
||||
|
||||
interface Options {
|
||||
dashboardUID: string;
|
||||
@ -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);
|
||||
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 } = useCombinedRules(dashboardUID, panelId, poll);
|
||||
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 { useCombinedRules } from '../hooks/useCombinedRuleNamespaces';
|
||||
|
||||
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 } = useCombinedRules(dashboardUid);
|
||||
const rules = grafanaNamespaces ? grafanaNamespaces.flatMap((ns) => ns.groups).flatMap((g) => g.rules) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -8,18 +8,19 @@ import { DataSourceApi } from '@grafana/data';
|
||||
import { PromOptions, PrometheusDatasource } from '@grafana/prometheus';
|
||||
import { locationService, setDataSourceSrv, setPluginExtensionsHook } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { fetchRules } from 'app/features/alerting/unified/api/prometheus';
|
||||
import { fetchRulerRules } from 'app/features/alerting/unified/api/ruler';
|
||||
import * as ruler from 'app/features/alerting/unified/api/ruler';
|
||||
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
||||
import * as alertingAbilities from 'app/features/alerting/unified/hooks/useAbilities';
|
||||
import { mockAlertRuleApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import {
|
||||
MockDataSourceSrv,
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
mockFolder,
|
||||
mockPromAlert,
|
||||
mockPromAlertingRule,
|
||||
mockPromRuleGroup,
|
||||
mockPromRuleNamespace,
|
||||
mockRulerGrafanaRule,
|
||||
mockRulerAlertingRule,
|
||||
mockRulerRuleGroup,
|
||||
} from 'app/features/alerting/unified/mocks';
|
||||
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
|
||||
import * as config from 'app/features/alerting/unified/utils/config';
|
||||
@ -29,11 +30,11 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
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 { createDashboardSceneFromDashboardModel } from '../../serialization/transformSaveModelToScene';
|
||||
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import * as utils from '../../utils/utils';
|
||||
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataAlertingTab';
|
||||
@ -47,6 +48,8 @@ jest.mock('app/features/alerting/unified/api/ruler');
|
||||
|
||||
jest.spyOn(config, 'getAllDataSources');
|
||||
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);
|
||||
jest.spyOn(ruler, 'rulerUrlBuilder');
|
||||
jest.spyOn(alertingAbilities, 'useAlertRuleAbility');
|
||||
|
||||
setPluginExtensionsHook(() => ({
|
||||
extensions: [],
|
||||
@ -70,10 +73,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(ruler.rulerUrlBuilder),
|
||||
};
|
||||
|
||||
const renderAlertTabContent = (model: PanelDataAlertingTab, initialStore?: ReturnType<typeof configureStore>) => {
|
||||
@ -84,73 +85,57 @@ const renderAlertTabContent = (model: PanelDataAlertingTab, initialStore?: Retur
|
||||
);
|
||||
};
|
||||
|
||||
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 = {
|
||||
default: [
|
||||
{
|
||||
name: 'mygroup',
|
||||
rules: [
|
||||
mockRulerGrafanaRule(
|
||||
{
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '12',
|
||||
[Annotation.panelID]: '34',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'dashboardrule1',
|
||||
}
|
||||
),
|
||||
],
|
||||
totals: {
|
||||
alerting: 2,
|
||||
},
|
||||
{
|
||||
name: 'othergroup',
|
||||
rules: [
|
||||
mockRulerGrafanaRule(
|
||||
{
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '121',
|
||||
[Annotation.panelID]: '341',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'dashboardrule2',
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dashboard = {
|
||||
@ -187,8 +172,10 @@ const ui = {
|
||||
row: byTestId('row'),
|
||||
createButton: byTestId<HTMLButtonElement>('create-alert-rule-button'),
|
||||
};
|
||||
const server = setupMswServer();
|
||||
|
||||
describe('PanelAlertTabContent', () => {
|
||||
// silenceConsoleOutput();
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
grantUserPermissions([
|
||||
@ -209,6 +196,42 @@ describe('PanelAlertTabContent', () => {
|
||||
) 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, {
|
||||
default: [
|
||||
mockRulerRuleGroup({
|
||||
name: 'mygroup',
|
||||
rules: [
|
||||
mockRulerAlertingRule({
|
||||
alert: 'dashboardrule1',
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '12',
|
||||
[Annotation.panelID]: '34',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
{
|
||||
name: 'othergroup',
|
||||
rules: [
|
||||
mockRulerAlertingRule({
|
||||
alert: 'dashboardrule2',
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '121',
|
||||
[Annotation.panelID]: '341',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Will take into account panel maxDataPoints', async () => {
|
||||
@ -289,18 +312,16 @@ describe('PanelAlertTabContent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// after updating to RTKQ, the response is already returning the alerts belonging to the panel
|
||||
it('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);
|
||||
|
||||
dashboard.panels = [panel];
|
||||
|
||||
renderAlertTab(dashboard);
|
||||
|
||||
const rows = await ui.row.findAll();
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]).toHaveTextContent(/dashboardrule1/);
|
||||
expect(rows[0]).not.toHaveTextContent(/dashboardrule2/);
|
||||
expect(rows[1]).toHaveTextContent(/dashboardrule2/);
|
||||
|
||||
const defaults = await clickNewButton();
|
||||
|
||||
@ -316,25 +337,6 @@ 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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2,18 +2,19 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
|
||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
||||
import { Alert, LoadingPlaceholder, Tab, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { RulesTable } from 'app/features/alerting/unified/components/rules/RulesTable';
|
||||
import { usePanelCombinedRules } from 'app/features/alerting/unified/hooks/usePanelCombinedRules';
|
||||
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
||||
import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
|
||||
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../../utils/utils';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { ScenesNewRuleFromPanelButton } from './NewAlertRuleButton';
|
||||
import { PanelDataPaneTabState, PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';
|
||||
import { PanelDataPaneTab, PanelDataPaneTabState, PanelDataTabHeaderProps, TabId } from './types';
|
||||
|
||||
export class PanelDataAlertingTab extends SceneObjectBase<PanelDataPaneTabState> implements PanelDataPaneTab {
|
||||
static Component = PanelDataAlertingTabRendered;
|
||||
@ -72,7 +73,7 @@ export function PanelDataAlertingTabRendered(props: SceneComponentProps<PanelDat
|
||||
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;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { from, map, Unsubscribable } from 'rxjs';
|
||||
import { from, map, Observable, Unsubscribable } from 'rxjs';
|
||||
|
||||
import { AlertState, AlertStateInfo, DataTopic, LoadingState, toDataFrame } from '@grafana/data';
|
||||
import { config, getBackendSrv } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
SceneDataLayerBase,
|
||||
SceneDataLayerProvider,
|
||||
@ -13,11 +13,15 @@ import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
|
||||
import { ungroupRulesByFileName } from 'app/features/alerting/unified/api/prometheus';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromAlertingRuleState, PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
import { RuleNamespace } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
@ -67,56 +71,55 @@ export class AlertStatesDataLayer
|
||||
if (!this.canWork(timeRange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alerStatesExecution = from(
|
||||
getBackendSrv().get(
|
||||
'/api/prometheus/grafana/api/v1/rules',
|
||||
{
|
||||
dashboard_uid: uid!,
|
||||
},
|
||||
`dashboard-query-runner-unified-alert-states-${id}`
|
||||
)
|
||||
).pipe(
|
||||
map((result: PromRulesResponse) => {
|
||||
if (result.status === 'success') {
|
||||
this.hasAlertRules = false;
|
||||
const panelIdToAlertState: Record<number, AlertStateInfo> = {};
|
||||
|
||||
result.data.groups.forEach((group) =>
|
||||
group.rules.forEach((rule) => {
|
||||
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
|
||||
this.hasAlertRules = true;
|
||||
const panelId = Number(rule.annotations[Annotation.panelID]);
|
||||
const state = promAlertStateToAlertState(rule.state);
|
||||
|
||||
// there can be multiple alerts per panel, so we make sure we get the most severe state:
|
||||
// alerting > pending > ok
|
||||
if (!panelIdToAlertState[panelId]) {
|
||||
panelIdToAlertState[panelId] = {
|
||||
state,
|
||||
id: Object.keys(panelIdToAlertState).length,
|
||||
panelId,
|
||||
dashboardId: id!,
|
||||
};
|
||||
} else if (
|
||||
state === AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Alerting;
|
||||
} else if (
|
||||
state === AlertState.Pending &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Pending
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Pending;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return Object.values(panelIdToAlertState);
|
||||
}
|
||||
|
||||
const fetchData: () => Promise<RuleNamespace[]> = async () => {
|
||||
const promRules = await dispatch(
|
||||
alertRuleApi.endpoints.prometheusRuleNamespaces.initiate({
|
||||
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
dashboardUid: uid,
|
||||
})
|
||||
);
|
||||
if (promRules.error) {
|
||||
throw new Error(`Unexpected alert rules response.`);
|
||||
}
|
||||
return promRules.data;
|
||||
};
|
||||
const res: Observable<PromRuleGroupDTO[]> = from(fetchData()).pipe(
|
||||
map((namespaces: RuleNamespace[]) => ungroupRulesByFileName(namespaces))
|
||||
);
|
||||
|
||||
const alerStatesExecution = res.pipe(
|
||||
map((groups: PromRuleGroupDTO[]) => {
|
||||
this.hasAlertRules = false;
|
||||
const panelIdToAlertState: Record<number, AlertStateInfo> = {};
|
||||
groups.forEach((group) =>
|
||||
group.rules.forEach((rule) => {
|
||||
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
|
||||
this.hasAlertRules = true;
|
||||
const panelId = Number(rule.annotations[Annotation.panelID]);
|
||||
const state = promAlertStateToAlertState(rule.state);
|
||||
|
||||
// there can be multiple alerts per panel, so we make sure we get the most severe state:
|
||||
// alerting > pending > ok
|
||||
if (!panelIdToAlertState[panelId]) {
|
||||
panelIdToAlertState[panelId] = {
|
||||
state,
|
||||
id: Object.keys(panelIdToAlertState).length,
|
||||
panelId,
|
||||
dashboardId: id!,
|
||||
};
|
||||
} else if (state === AlertState.Alerting && panelIdToAlertState[panelId].state !== AlertState.Alerting) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Alerting;
|
||||
} else if (
|
||||
state === AlertState.Pending &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Pending
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Pending;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return Object.values(panelIdToAlertState);
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -3,9 +3,15 @@ import { delay, first } from 'rxjs/operators';
|
||||
|
||||
import { AlertState } from '@grafana/data';
|
||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
|
||||
import {
|
||||
grantUserPermissions,
|
||||
mockPromAlertingRule,
|
||||
mockPromRuleGroup,
|
||||
mockPromRuleNamespace,
|
||||
} from 'app/features/alerting/unified/mocks';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import * as store from 'app/store/store';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromAlertingRuleState, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
|
||||
@ -22,6 +28,39 @@ jest.mock('@grafana/runtime', () => ({
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
const nameSpaces = [
|
||||
mockPromRuleNamespace({
|
||||
groups: [
|
||||
mockPromRuleGroup({
|
||||
name: 'my-group',
|
||||
rules: [
|
||||
mockPromAlertingRule({
|
||||
name: 'my alert',
|
||||
state: PromAlertingRuleState.Firing,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '1',
|
||||
[Annotation.panelID]: '1',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
mockPromRuleGroup({
|
||||
name: 'another-group',
|
||||
rules: [
|
||||
mockPromAlertingRule({
|
||||
name: 'another alert',
|
||||
state: PromAlertingRuleState.Firing,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: '1',
|
||||
[Annotation.panelID]: '2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]);
|
||||
});
|
||||
@ -105,8 +144,9 @@ function getTestContext() {
|
||||
},
|
||||
} as DataSourceSrv;
|
||||
setDataSourceSrv(dataSourceSrvMock);
|
||||
const dispatchMock = jest.spyOn(store, 'dispatch');
|
||||
|
||||
return { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock };
|
||||
return { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock, dispatchMock };
|
||||
}
|
||||
|
||||
function expectOnResults(args: {
|
||||
@ -134,7 +174,8 @@ function expectOnResults(args: {
|
||||
describe('DashboardQueryRunnerImpl', () => {
|
||||
describe('when calling run and all workers succeed', () => {
|
||||
it('then it should return the correct results', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
const { dispatchMock, runner, options, annotationQueryMock, executeAnnotationQueryMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ data: nameSpaces });
|
||||
|
||||
expectOnResults({
|
||||
runner,
|
||||
@ -146,7 +187,7 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual(getExpectedForAllResult());
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
|
||||
@ -156,10 +197,10 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
|
||||
describe('when calling run and all workers succeed but take longer than 200ms', () => {
|
||||
it('then it should return the empty results', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, dispatchMock } = getTestContext();
|
||||
const wait = 201;
|
||||
executeAnnotationQueryMock.mockReturnValue(toAsyncOfResult({ events: [{ id: 'NextGen' }] }).pipe(delay(wait)));
|
||||
|
||||
dispatchMock.mockResolvedValue({ data: nameSpaces });
|
||||
expectOnResults({
|
||||
runner,
|
||||
panelId: 1,
|
||||
@ -170,7 +211,7 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual({ annotations: [] });
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
|
||||
@ -180,8 +221,8 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
|
||||
describe('when calling run and all workers succeed but the subscriber subscribes after the run', () => {
|
||||
it('then it should return the last results', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ data: nameSpaces });
|
||||
runner.run(options);
|
||||
|
||||
setTimeout(
|
||||
@ -196,7 +237,7 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual(getExpectedForAllResult());
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
}),
|
||||
200
|
||||
@ -207,8 +248,8 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
describe('when calling run and all workers fail', () => {
|
||||
silenceConsoleOutput();
|
||||
it('then it should return the correct results', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
getMock.mockRejectedValue({ message: 'Get error' });
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ error: { message: 'Get error' } });
|
||||
annotationQueryMock.mockRejectedValue({ message: 'Legacy error' });
|
||||
executeAnnotationQueryMock.mockReturnValue(throwError({ message: 'NextGen error' }));
|
||||
|
||||
@ -223,7 +264,6 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual(expected);
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
|
||||
@ -234,8 +274,8 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
describe('when calling run and AlertStatesWorker fails', () => {
|
||||
silenceConsoleOutput();
|
||||
it('then it should return the correct results', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
getMock.mockRejectedValue({ message: 'Get error' });
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ message: 'Get error' });
|
||||
|
||||
expectOnResults({
|
||||
runner,
|
||||
@ -249,7 +289,6 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual(expected);
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
|
||||
@ -259,7 +298,8 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
describe('when calling run and AnnotationsWorker fails', () => {
|
||||
silenceConsoleOutput();
|
||||
it('then it should return the correct results', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ data: nameSpaces });
|
||||
annotationQueryMock.mockRejectedValue({ message: 'Legacy error' });
|
||||
executeAnnotationQueryMock.mockReturnValue(throwError({ message: 'NextGen error' }));
|
||||
|
||||
@ -275,7 +315,6 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual(expected);
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
|
||||
@ -286,7 +325,8 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
|
||||
describe('when calling run twice', () => {
|
||||
it('then it should cancel previous run', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ data: nameSpaces });
|
||||
executeAnnotationQueryMock.mockReturnValueOnce(
|
||||
toAsyncOfResult({ events: [{ id: 'NextGen' }] }).pipe(delay(10000))
|
||||
);
|
||||
@ -303,7 +343,7 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual(expected);
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(2);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(2);
|
||||
expect(getMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
});
|
||||
|
||||
@ -314,7 +354,8 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
|
||||
describe('when calling cancel', () => {
|
||||
it('then it should cancel matching workers', (done) => {
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
|
||||
const { runner, options, annotationQueryMock, executeAnnotationQueryMock, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ data: nameSpaces });
|
||||
executeAnnotationQueryMock.mockReturnValueOnce(
|
||||
toAsyncOfResult({ events: [{ id: 'NextGen' }] }).pipe(delay(10000))
|
||||
);
|
||||
@ -330,7 +371,6 @@ describe('DashboardQueryRunnerImpl', () => {
|
||||
expect(results).toEqual({ alertState, annotations: [annotations[0], annotations[2]] });
|
||||
expect(annotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -3,11 +3,16 @@ import { lastValueFrom } from 'rxjs';
|
||||
import { AlertState, getDefaultTimeRange, TimeRange } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
|
||||
import {
|
||||
grantUserPermissions,
|
||||
mockPromAlertingRule,
|
||||
mockPromRuleGroup,
|
||||
mockPromRuleNamespace,
|
||||
} from 'app/features/alerting/unified/mocks';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
import { PromAlertingRuleState, PromRuleDTO, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
||||
import * as store from '../../../../store/store';
|
||||
@ -37,9 +42,8 @@ function getTestContext() {
|
||||
jest.clearAllMocks();
|
||||
const dispatchMock = jest.spyOn(store, 'dispatch');
|
||||
const options = getDefaultOptions();
|
||||
const getMock = jest.spyOn(backendSrv, 'get');
|
||||
|
||||
return { getMock, options, dispatchMock };
|
||||
return { options, dispatchMock };
|
||||
}
|
||||
|
||||
describe('UnifiedAlertStatesWorker', () => {
|
||||
@ -88,30 +92,23 @@ describe('UnifiedAlertStatesWorker', () => {
|
||||
|
||||
describe('when run is called with incorrect props', () => {
|
||||
it('then it should return the correct results', async () => {
|
||||
const { getMock, options } = getTestContext();
|
||||
const { options } = getTestContext();
|
||||
const dashboard = createDashboardModelFixture({});
|
||||
|
||||
await expect(worker.work({ ...options, dashboard })).toEmitValuesWith((received) => {
|
||||
expect(received).toHaveLength(1);
|
||||
const results = received[0];
|
||||
expect(results).toEqual({ alertStates: [], annotations: [] });
|
||||
expect(getMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when run repeatedly for the same dashboard and no alert rules are found', () => {
|
||||
const nameSpaces = [mockPromRuleNamespace({ groups: [] })];
|
||||
const { dispatchMock, options } = getTestContext();
|
||||
dispatchMock.mockResolvedValue(nameSpaces);
|
||||
it('then canWork should start returning false', async () => {
|
||||
const worker = new UnifiedAlertStatesWorker();
|
||||
|
||||
const getResults: PromRulesResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
groups: [],
|
||||
},
|
||||
};
|
||||
const { getMock, options } = getTestContext();
|
||||
getMock.mockResolvedValue(getResults);
|
||||
expect(worker.canWork(options)).toBe(true);
|
||||
await lastValueFrom(worker.work(options));
|
||||
expect(worker.canWork(options)).toBe(false);
|
||||
@ -119,45 +116,41 @@ describe('UnifiedAlertStatesWorker', () => {
|
||||
});
|
||||
|
||||
describe('when run is called with correct props and request is successful', () => {
|
||||
function mockPromRuleDTO(overrides: Partial<PromRuleDTO>): PromRuleDTO {
|
||||
return {
|
||||
alerts: [],
|
||||
health: 'ok',
|
||||
name: 'foo',
|
||||
query: 'foo',
|
||||
type: PromRuleType.Alerting,
|
||||
state: PromAlertingRuleState.Firing,
|
||||
labels: {},
|
||||
annotations: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
it('then it should return the correct results', async () => {
|
||||
const getResults: PromRulesResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
const nameSpaces = [
|
||||
mockPromRuleNamespace({
|
||||
groups: [
|
||||
{
|
||||
name: 'group',
|
||||
file: '',
|
||||
interval: 1,
|
||||
mockPromRuleGroup({
|
||||
name: 'group1',
|
||||
rules: [
|
||||
mockPromRuleDTO({
|
||||
mockPromAlertingRule({
|
||||
name: 'alert1',
|
||||
state: PromAlertingRuleState.Firing,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: 'a uid',
|
||||
[Annotation.panelID]: '1',
|
||||
},
|
||||
}),
|
||||
mockPromRuleDTO({
|
||||
],
|
||||
}),
|
||||
mockPromRuleGroup({
|
||||
name: 'group2',
|
||||
rules: [
|
||||
mockPromAlertingRule({
|
||||
name: 'alert2',
|
||||
state: PromAlertingRuleState.Inactive,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: 'a uid',
|
||||
[Annotation.panelID]: '2',
|
||||
},
|
||||
}),
|
||||
mockPromRuleDTO({
|
||||
],
|
||||
}),
|
||||
mockPromRuleGroup({
|
||||
name: 'group3',
|
||||
rules: [
|
||||
mockPromAlertingRule({
|
||||
name: 'alert3',
|
||||
state: PromAlertingRuleState.Pending,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: 'a uid',
|
||||
@ -165,12 +158,12 @@ describe('UnifiedAlertStatesWorker', () => {
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
const { getMock, options } = getTestContext();
|
||||
getMock.mockResolvedValue(getResults);
|
||||
}),
|
||||
];
|
||||
const { dispatchMock, options } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ data: nameSpaces });
|
||||
|
||||
await expect(worker.work(options)).toEmitValuesWith((received) => {
|
||||
expect(received).toHaveLength(1);
|
||||
@ -184,43 +177,33 @@ describe('UnifiedAlertStatesWorker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledWith(
|
||||
'/api/prometheus/grafana/api/v1/rules',
|
||||
{ dashboard_uid: 'a uid' },
|
||||
'dashboard-query-runner-unified-alert-states-12345'
|
||||
);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when run is called with correct props and request fails', () => {
|
||||
silenceConsoleOutput();
|
||||
it('then it should return the correct results', async () => {
|
||||
const { getMock, options, dispatchMock } = getTestContext();
|
||||
getMock.mockRejectedValue({ message: 'An error' });
|
||||
const { options, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ error: 'An error' });
|
||||
|
||||
await expect(worker.work(options)).toEmitValuesWith((received) => {
|
||||
expect(received).toHaveLength(1);
|
||||
const results = received[0];
|
||||
expect(results).toEqual({ alertStates: [], annotations: [] });
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when run is called with correct props and request is cancelled', () => {
|
||||
silenceConsoleOutput();
|
||||
it('then it should return the correct results', async () => {
|
||||
const { getMock, options, dispatchMock } = getTestContext();
|
||||
getMock.mockRejectedValue({ cancelled: true });
|
||||
const { options, dispatchMock } = getTestContext();
|
||||
dispatchMock.mockResolvedValue({ error: { message: 'Get error' } });
|
||||
|
||||
await expect(worker.work(options)).toEmitValuesWith((received) => {
|
||||
expect(received).toHaveLength(1);
|
||||
const results = received[0];
|
||||
expect(results).toEqual({ alertStates: [], annotations: [] });
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { Observable, from } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
import { AlertState, AlertStateInfo } from '@grafana/data';
|
||||
import { config, getBackendSrv } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
|
||||
import { ungroupRulesByFileName } from 'app/features/alerting/unified/api/prometheus';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import { promAlertStateToAlertState } from 'app/features/dashboard-scene/scene/AlertStatesDataLayer';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
import { RuleNamespace } from 'app/types/unified-alerting';
|
||||
import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorker, DashboardQueryRunnerWorkerResult } from './types';
|
||||
import { emptyResult, handleDashboardQueryRunnerWorkerError } from './utils';
|
||||
@ -54,53 +59,53 @@ export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker {
|
||||
}
|
||||
|
||||
const { dashboard } = options;
|
||||
return from(
|
||||
getBackendSrv().get(
|
||||
'/api/prometheus/grafana/api/v1/rules',
|
||||
{
|
||||
dashboard_uid: dashboard.uid,
|
||||
},
|
||||
`dashboard-query-runner-unified-alert-states-${dashboard.id}`
|
||||
)
|
||||
).pipe(
|
||||
map((result: PromRulesResponse) => {
|
||||
if (result.status === 'success') {
|
||||
this.hasAlertRules[dashboard.uid] = false;
|
||||
const panelIdToAlertState: Record<number, AlertStateInfo> = {};
|
||||
result.data.groups.forEach((group) =>
|
||||
group.rules.forEach((rule) => {
|
||||
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
|
||||
this.hasAlertRules[dashboard.uid] = true;
|
||||
const panelId = Number(rule.annotations[Annotation.panelID]);
|
||||
const state = promAlertStateToAlertState(rule.state);
|
||||
const fetchData: () => Promise<RuleNamespace[]> = async () => {
|
||||
const promRules = await dispatch(
|
||||
alertRuleApi.endpoints.prometheusRuleNamespaces.initiate({
|
||||
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
dashboardUid: dashboard.uid,
|
||||
})
|
||||
);
|
||||
return promRules.data;
|
||||
};
|
||||
|
||||
// there can be multiple alerts per panel, so we make sure we get the most severe state:
|
||||
// alerting > pending > ok
|
||||
if (!panelIdToAlertState[panelId]) {
|
||||
panelIdToAlertState[panelId] = {
|
||||
state,
|
||||
id: Object.keys(panelIdToAlertState).length,
|
||||
panelId,
|
||||
dashboardId: dashboard.id,
|
||||
};
|
||||
} else if (
|
||||
state === AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Alerting;
|
||||
} else if (
|
||||
state === AlertState.Pending &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Pending
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Pending;
|
||||
}
|
||||
const res: Observable<PromRuleGroupDTO[]> = from(fetchData()).pipe(
|
||||
map((namespaces: RuleNamespace[]) => ungroupRulesByFileName(namespaces))
|
||||
);
|
||||
|
||||
return res.pipe(
|
||||
map((groups: PromRuleGroupDTO[]) => {
|
||||
this.hasAlertRules[dashboard.uid] = false;
|
||||
const panelIdToAlertState: Record<number, AlertStateInfo> = {};
|
||||
groups.forEach((group) =>
|
||||
group.rules.forEach((rule) => {
|
||||
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
|
||||
this.hasAlertRules[dashboard.uid] = true;
|
||||
const panelId = Number(rule.annotations[Annotation.panelID]);
|
||||
const state = promAlertStateToAlertState(rule.state);
|
||||
|
||||
// there can be multiple alerts per panel, so we make sure we get the most severe state:
|
||||
// alerting > pending > ok
|
||||
if (!panelIdToAlertState[panelId]) {
|
||||
panelIdToAlertState[panelId] = {
|
||||
state,
|
||||
id: Object.keys(panelIdToAlertState).length,
|
||||
panelId,
|
||||
dashboardId: dashboard.id,
|
||||
};
|
||||
} else if (state === AlertState.Alerting && panelIdToAlertState[panelId].state !== AlertState.Alerting) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Alerting;
|
||||
} else if (
|
||||
state === AlertState.Pending &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Pending
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Pending;
|
||||
}
|
||||
})
|
||||
);
|
||||
return { alertStates: Object.values(panelIdToAlertState), annotations: [] };
|
||||
}
|
||||
throw new Error(`Unexpected alert rules response.`);
|
||||
}
|
||||
})
|
||||
);
|
||||
return { alertStates: Object.values(panelIdToAlertState), annotations: [] };
|
||||
}),
|
||||
catchError(handleDashboardQueryRunnerWorkerError)
|
||||
);
|
||||
|
Reference in New Issue
Block a user