mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: make alert state indicator in panel header work with Grafana 8 alerts (#38713)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @internal -- might be replaced by next generation Alerting
|
||||
* @internal
|
||||
*/
|
||||
export enum AlertState {
|
||||
NoData = 'no_data',
|
||||
@@ -11,12 +11,11 @@ export enum AlertState {
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal -- might be replaced by next generation Alerting
|
||||
* @internal
|
||||
*/
|
||||
export interface AlertStateInfo {
|
||||
id: number;
|
||||
dashboardId: number;
|
||||
panelId: number;
|
||||
state: AlertState;
|
||||
newStateDate: string;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ export interface PanelData {
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @deprecated alertState is deprecated and will be removed when the next generation Alerting is in place
|
||||
*/
|
||||
alertState?: AlertStateInfo;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
mockPromRuleNamespace,
|
||||
mockRulerGrafanaRule,
|
||||
} from './mocks';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||
import { getAllDataSources } from './utils/config';
|
||||
import { fetchRules } from './api/prometheus';
|
||||
@@ -270,5 +270,14 @@ describe('PanelAlertTabContent', () => {
|
||||
{ key: '__panelId__', value: '34' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(mocks.api.fetchRulerRules).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||
dashboardUID: dashboard.uid,
|
||||
panelId: panel.editSourceId,
|
||||
});
|
||||
expect(mocks.api.fetchRules).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||
dashboardUID: dashboard.uid,
|
||||
panelId: panel.editSourceId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,14 +3,32 @@ import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { RuleNamespace } from 'app/types/unified-alerting';
|
||||
import { PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
import { getDatasourceAPIId } from '../utils/datasource';
|
||||
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
export interface FetchPromRulesFilter {
|
||||
dashboardUID: string;
|
||||
panelId?: number;
|
||||
}
|
||||
|
||||
export async function fetchRules(dataSourceName: string, filter?: FetchPromRulesFilter): Promise<RuleNamespace[]> {
|
||||
if (filter?.dashboardUID && dataSourceName !== GRAFANA_RULES_SOURCE_NAME) {
|
||||
throw new Error('Filtering by dashboard UID is not supported for cloud rules sources.');
|
||||
}
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
if (filter?.dashboardUID) {
|
||||
params['dashboard_uid'] = filter.dashboardUID;
|
||||
if (filter.panelId) {
|
||||
params['panel_id'] = String(filter.panelId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRules(dataSourceName: string): Promise<RuleNamespace[]> {
|
||||
const response = await lastValueFrom(
|
||||
getBackendSrv().fetch<PromRulesResponse>({
|
||||
url: `/api/prometheus/${getDatasourceAPIId(dataSourceName)}/api/v1/rules`,
|
||||
showErrorAlert: false,
|
||||
showSuccessAlert: false,
|
||||
params,
|
||||
})
|
||||
).catch((e) => {
|
||||
if ('status' in e && e.status === 404) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lastValueFrom } from 'rxjs';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { PostableRulerRuleGroupDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
import { getDatasourceAPIId } from '../utils/datasource';
|
||||
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
|
||||
|
||||
// upsert a rule group. use this to update rules
|
||||
@@ -22,9 +22,29 @@ export async function setRulerRuleGroup(
|
||||
);
|
||||
}
|
||||
|
||||
export interface FetchRulerRulesFilter {
|
||||
dashboardUID: string;
|
||||
panelId?: number;
|
||||
}
|
||||
|
||||
// fetch all ruler rule namespaces and included groups
|
||||
export async function fetchRulerRules(dataSourceName: string) {
|
||||
return rulerGetRequest<RulerRulesConfigDTO>(`/api/ruler/${getDatasourceAPIId(dataSourceName)}/api/v1/rules`, {});
|
||||
export async function fetchRulerRules(dataSourceName: string, filter?: FetchRulerRulesFilter) {
|
||||
if (filter?.dashboardUID && dataSourceName !== GRAFANA_RULES_SOURCE_NAME) {
|
||||
throw new Error('Filtering by dashboard UID is not supported for cloud rules sources.');
|
||||
}
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
if (filter?.dashboardUID) {
|
||||
params['dashboard_uid'] = filter.dashboardUID;
|
||||
if (filter.panelId) {
|
||||
params['panel_id'] = String(filter.panelId);
|
||||
}
|
||||
}
|
||||
return rulerGetRequest<RulerRulesConfigDTO>(
|
||||
`/api/ruler/${getDatasourceAPIId(dataSourceName)}/api/v1/rules`,
|
||||
{},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
// fetch rule groups for a particular namespace
|
||||
@@ -66,13 +86,14 @@ export async function deleteRulerRulesGroup(dataSourceName: string, namespace: s
|
||||
}
|
||||
|
||||
// false in case ruler is not supported. this is weird, but we'll work on it
|
||||
async function rulerGetRequest<T>(url: string, empty: T): Promise<T> {
|
||||
async function rulerGetRequest<T>(url: string, empty: T, params?: Record<string, string>): Promise<T> {
|
||||
try {
|
||||
const response = await lastValueFrom(
|
||||
getBackendSrv().fetch<T>({
|
||||
url,
|
||||
showErrorAlert: false,
|
||||
showSuccessAlert: false,
|
||||
params,
|
||||
})
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -130,7 +130,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
)}
|
||||
</div>
|
||||
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) &&
|
||||
dataSourceName && <GroupAndNamespaceFields dataSourceName={dataSourceName} />}
|
||||
dataSourceName && <GroupAndNamespaceFields rulesSourceName={dataSourceName} />}
|
||||
|
||||
{ruleFormType === RuleFormType.grafana && (
|
||||
<Field
|
||||
|
||||
@@ -10,10 +10,10 @@ import { Field, InputControl, useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
interface Props {
|
||||
dataSourceName: string;
|
||||
rulesSourceName: string;
|
||||
}
|
||||
|
||||
export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
||||
export const GroupAndNamespaceFields: FC<Props> = ({ rulesSourceName }) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
@@ -28,10 +28,10 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
||||
const rulerRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(fetchRulerRulesAction(dataSourceName));
|
||||
}, [dataSourceName, dispatch]);
|
||||
dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||
}, [rulesSourceName, dispatch]);
|
||||
|
||||
const rulesConfig = rulerRequests[dataSourceName]?.result;
|
||||
const rulesConfig = rulerRequests[rulesSourceName]?.result;
|
||||
|
||||
const namespace = watch('namespace');
|
||||
|
||||
|
||||
@@ -75,21 +75,21 @@ export function useCombinedRulesMatching(
|
||||
};
|
||||
}
|
||||
|
||||
function useCombinedRulesLoader(ruleSourceName: string | undefined): AsyncRequestState<void> {
|
||||
function useCombinedRulesLoader(rulesSourceName: string | undefined): AsyncRequestState<void> {
|
||||
const dispatch = useDispatch();
|
||||
const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules);
|
||||
const promRuleRequest = getRequestState(ruleSourceName, promRuleRequests);
|
||||
const promRuleRequest = getRequestState(rulesSourceName, promRuleRequests);
|
||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
const rulerRuleRequest = getRequestState(ruleSourceName, rulerRuleRequests);
|
||||
const rulerRuleRequest = getRequestState(rulesSourceName, rulerRuleRequests);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ruleSourceName) {
|
||||
if (!rulesSourceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchPromRulesAction(ruleSourceName));
|
||||
dispatch(fetchRulerRulesAction(ruleSourceName));
|
||||
}, [dispatch, ruleSourceName]);
|
||||
dispatch(fetchPromRulesAction({ rulesSourceName }));
|
||||
dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||
}, [dispatch, rulesSourceName]);
|
||||
|
||||
return {
|
||||
loading: promRuleRequest.loading || rulerRuleRequest.loading,
|
||||
|
||||
@@ -34,8 +34,18 @@ export function usePanelCombinedRules({ dashboard, panel, poll = false }: Option
|
||||
// fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS
|
||||
useEffect(() => {
|
||||
const fetch = () => {
|
||||
dispatch(fetchPromRulesAction(GRAFANA_RULES_SOURCE_NAME));
|
||||
dispatch(fetchRulerRulesAction(GRAFANA_RULES_SOURCE_NAME));
|
||||
dispatch(
|
||||
fetchPromRulesAction({
|
||||
rulesSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
filter: { dashboardUID: dashboard.uid, panelId: panel.editSourceId },
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
fetchRulerRulesAction({
|
||||
rulesSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
filter: { dashboardUID: dashboard.uid, panelId: panel.editSourceId },
|
||||
})
|
||||
);
|
||||
};
|
||||
fetch();
|
||||
if (poll) {
|
||||
@@ -45,7 +55,7 @@ export function usePanelCombinedRules({ dashboard, panel, poll = false }: Option
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, [dispatch, poll]);
|
||||
}, [dispatch, poll, panel.editSourceId, dashboard.uid]);
|
||||
|
||||
const loading = promRuleRequest.loading || rulerRuleRequest.loading;
|
||||
const errors = [promRuleRequest.error, rulerRuleRequest.error].filter(
|
||||
|
||||
@@ -29,13 +29,14 @@ import {
|
||||
deleteAlertManagerConfig,
|
||||
testReceivers,
|
||||
} from '../api/alertmanager';
|
||||
import { fetchRules } from '../api/prometheus';
|
||||
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
||||
import {
|
||||
deleteNamespace,
|
||||
deleteRulerRulesGroup,
|
||||
fetchRulerRules,
|
||||
fetchRulerRulesGroup,
|
||||
fetchRulerRulesNamespace,
|
||||
FetchRulerRulesFilter,
|
||||
setRulerRuleGroup,
|
||||
} from '../api/ruler';
|
||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
@@ -64,7 +65,8 @@ const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
|
||||
|
||||
export const fetchPromRulesAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchPromRules',
|
||||
(rulesSourceName: string): Promise<RuleNamespace[]> => withSerializedError(fetchRules(rulesSourceName))
|
||||
({ rulesSourceName, filter }: { rulesSourceName: string; filter?: FetchPromRulesFilter }): Promise<RuleNamespace[]> =>
|
||||
withSerializedError(fetchRules(rulesSourceName, filter))
|
||||
);
|
||||
|
||||
export const fetchAlertManagerConfigAction = createAsyncThunk(
|
||||
@@ -106,8 +108,14 @@ export const fetchAlertManagerConfigAction = createAsyncThunk(
|
||||
|
||||
export const fetchRulerRulesAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchRulerRules',
|
||||
(rulesSourceName: string): Promise<RulerRulesConfigDTO | null> => {
|
||||
return withSerializedError(fetchRulerRules(rulesSourceName));
|
||||
({
|
||||
rulesSourceName,
|
||||
filter,
|
||||
}: {
|
||||
rulesSourceName: string;
|
||||
filter?: FetchRulerRulesFilter;
|
||||
}): Promise<RulerRulesConfigDTO | null> => {
|
||||
return withSerializedError(fetchRulerRules(rulesSourceName, filter));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -119,12 +127,12 @@ export const fetchSilencesAction = createAsyncThunk(
|
||||
);
|
||||
|
||||
// this will only trigger ruler rules fetch if rules are not loaded yet and request is not in flight
|
||||
export function fetchRulerRulesIfNotFetchedYet(dataSourceName: string): ThunkResult<void> {
|
||||
export function fetchRulerRulesIfNotFetchedYet(rulesSourceName: string): ThunkResult<void> {
|
||||
return (dispatch, getStore) => {
|
||||
const { rulerRules } = getStore().unifiedAlerting;
|
||||
const resp = rulerRules[dataSourceName];
|
||||
const resp = rulerRules[rulesSourceName];
|
||||
if (!resp?.result && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) {
|
||||
dispatch(fetchRulerRulesAction(dataSourceName));
|
||||
dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -132,12 +140,12 @@ export function fetchRulerRulesIfNotFetchedYet(dataSourceName: string): ThunkRes
|
||||
export function fetchAllPromAndRulerRulesAction(force = false): ThunkResult<void> {
|
||||
return (dispatch, getStore) => {
|
||||
const { promRules, rulerRules } = getStore().unifiedAlerting;
|
||||
getAllRulesSourceNames().map((name) => {
|
||||
if (force || !promRules[name]?.loading) {
|
||||
dispatch(fetchPromRulesAction(name));
|
||||
getAllRulesSourceNames().map((rulesSourceName) => {
|
||||
if (force || !promRules[rulesSourceName]?.loading) {
|
||||
dispatch(fetchPromRulesAction({ rulesSourceName }));
|
||||
}
|
||||
if (force || !rulerRules[name]?.loading) {
|
||||
dispatch(fetchRulerRulesAction(name));
|
||||
if (force || !rulerRules[rulesSourceName]?.loading) {
|
||||
dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -146,9 +154,9 @@ export function fetchAllPromAndRulerRulesAction(force = false): ThunkResult<void
|
||||
export function fetchAllPromRulesAction(force = false): ThunkResult<void> {
|
||||
return (dispatch, getStore) => {
|
||||
const { promRules } = getStore().unifiedAlerting;
|
||||
getAllRulesSourceNames().map((name) => {
|
||||
if (force || !promRules[name]?.loading) {
|
||||
dispatch(fetchPromRulesAction(name));
|
||||
getAllRulesSourceNames().map((rulesSourceName) => {
|
||||
if (force || !promRules[rulesSourceName]?.loading) {
|
||||
dispatch(fetchPromRulesAction({ rulesSourceName }));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -250,8 +258,8 @@ export function deleteRuleAction(
|
||||
}
|
||||
await deleteRule(ruleWithLocation);
|
||||
// refetch rules for this rules source
|
||||
dispatch(fetchRulerRulesAction(ruleWithLocation.ruleSourceName));
|
||||
dispatch(fetchPromRulesAction(ruleWithLocation.ruleSourceName));
|
||||
dispatch(fetchRulerRulesAction({ rulesSourceName: ruleWithLocation.ruleSourceName }));
|
||||
dispatch(fetchPromRulesAction({ rulesSourceName: ruleWithLocation.ruleSourceName }));
|
||||
|
||||
if (options.navigateTo) {
|
||||
locationService.replace(options.navigateTo);
|
||||
@@ -712,7 +720,7 @@ export const updateLotexNamespaceAndGroupAction = createAsyncThunk(
|
||||
}
|
||||
|
||||
// refetch all rules
|
||||
await thunkAPI.dispatch(fetchRulerRulesAction(rulesSourceName));
|
||||
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||
})()
|
||||
),
|
||||
{
|
||||
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
} from './actions';
|
||||
|
||||
export const reducer = combineReducers({
|
||||
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, (dataSourceName) => dataSourceName).reducer,
|
||||
rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, (dataSourceName) => dataSourceName).reducer,
|
||||
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, ({ rulesSourceName }) => rulesSourceName).reducer,
|
||||
rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, ({ rulesSourceName }) => rulesSourceName)
|
||||
.reducer,
|
||||
amConfigs: createAsyncMapSlice(
|
||||
'amConfigs',
|
||||
fetchAlertManagerConfigAction,
|
||||
|
||||
@@ -36,6 +36,8 @@ export const PanelEditorTabs: FC<PanelEditorTabsProps> = React.memo(({ panel, da
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(config.unifiedAlertingEnabled, tabs);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<TabsBar className={styles.tabBar} hideBorder>
|
||||
|
||||
@@ -34,7 +34,10 @@ export const getPanelEditorTabs = memoizeOne((tab?: string, plugin?: PanelPlugin
|
||||
});
|
||||
}
|
||||
|
||||
if ((getConfig().alertingEnabled && plugin.meta.id === 'graph') || plugin.meta.id === 'timeseries') {
|
||||
if (
|
||||
((getConfig().alertingEnabled || getConfig().unifiedAlertingEnabled) && plugin.meta.id === 'graph') ||
|
||||
plugin.meta.id === 'timeseries'
|
||||
) {
|
||||
tabs.push({
|
||||
id: PanelEditorTabId.Alert,
|
||||
text: 'Alert',
|
||||
|
||||
@@ -475,7 +475,7 @@ export class PanelChrome extends Component<Props, State> {
|
||||
const { errorMessage, data } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
let alertState = config.unifiedAlertingEnabled ? undefined : data.alertState?.state;
|
||||
const alertState = data.alertState?.state;
|
||||
|
||||
const containerClassNames = classNames({
|
||||
'panel-container': true,
|
||||
|
||||
@@ -186,7 +186,7 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
const { errorMessage, data } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
let alertState = config.unifiedAlertingEnabled ? undefined : data.alertState?.state;
|
||||
const alertState = data.alertState?.state;
|
||||
|
||||
const containerClassNames = classNames({
|
||||
'panel-container': true,
|
||||
|
||||
@@ -12,7 +12,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}));
|
||||
|
||||
function getDefaultOptions(): DashboardQueryRunnerOptions {
|
||||
const dashboard: any = { id: 'an id' };
|
||||
const dashboard: any = { id: 'an id', panels: [{ alert: {} }] };
|
||||
const range = getDefaultTimeRange();
|
||||
|
||||
return { dashboard, range };
|
||||
@@ -57,6 +57,14 @@ describe('AlertStatesWorker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canWork is called for dashboard with no alert panels', () => {
|
||||
it('then it should return false', () => {
|
||||
const options = getDefaultOptions();
|
||||
options.dashboard.panels.forEach((panel) => delete panel.alert);
|
||||
expect(worker.canWork(options)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when run is called with incorrect props', () => {
|
||||
it('then it should return the correct results', async () => {
|
||||
const { getMock, options } = getTestContext();
|
||||
@@ -74,8 +82,8 @@ describe('AlertStatesWorker', () => {
|
||||
describe('when run is called with correct props and request is successful', () => {
|
||||
it('then it should return the correct results', async () => {
|
||||
const getResults: AlertStateInfo[] = [
|
||||
{ id: 1, state: AlertState.Alerting, newStateDate: '2021-01-01', dashboardId: 1, panelId: 1 },
|
||||
{ id: 2, state: AlertState.Alerting, newStateDate: '2021-02-01', dashboardId: 1, panelId: 2 },
|
||||
{ id: 1, state: AlertState.Alerting, dashboardId: 1, panelId: 1 },
|
||||
{ id: 2, state: AlertState.Alerting, dashboardId: 1, panelId: 2 },
|
||||
];
|
||||
const { getMock, options } = getTestContext();
|
||||
getMock.mockResolvedValue(getResults);
|
||||
|
||||
@@ -14,6 +14,11 @@ export class AlertStatesWorker implements DashboardQueryRunnerWorker {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if dashboard has no alerts, no point to query alert states
|
||||
if (!dashboard.panels.find((panel) => !!panel.alert)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ function getTestContext() {
|
||||
const runner = createDashboardQueryRunner({ dashboard: options.dashboard, timeSrv: timeSrvMock });
|
||||
|
||||
const getResults: AlertStateInfo[] = [
|
||||
{ id: 1, state: AlertState.Alerting, newStateDate: '2021-01-01', dashboardId: 1, panelId: 1 },
|
||||
{ id: 2, state: AlertState.Alerting, newStateDate: '2021-02-01', dashboardId: 1, panelId: 2 },
|
||||
{ id: 1, state: AlertState.Alerting, dashboardId: 1, panelId: 1 },
|
||||
{ id: 2, state: AlertState.Alerting, dashboardId: 1, panelId: 2 },
|
||||
];
|
||||
const getMock = jest.spyOn(backendSrv, 'get').mockResolvedValue(getResults);
|
||||
const executeAnnotationQueryMock = jest
|
||||
@@ -291,7 +291,6 @@ function getExpectedForAllResult(): DashboardQueryRunnerResult {
|
||||
alertState: {
|
||||
dashboardId: 1,
|
||||
id: 1,
|
||||
newStateDate: '2021-01-01',
|
||||
panelId: 1,
|
||||
state: AlertState.Alerting,
|
||||
},
|
||||
|
||||
@@ -17,6 +17,8 @@ import { getAnnotationsByPanelId } from './utils';
|
||||
import { DashboardModel } from '../../../dashboard/state';
|
||||
import { getTimeSrv, TimeSrv } from '../../../dashboard/services/TimeSrv';
|
||||
import { RefreshEvent } from '../../../../types/events';
|
||||
import { config } from 'app/core/config';
|
||||
import { UnifiedAlertStatesWorker } from './UnifiedAlertStatesWorker';
|
||||
|
||||
class DashboardQueryRunnerImpl implements DashboardQueryRunner {
|
||||
private readonly results: ReplaySubject<DashboardQueryRunnerWorkerResult>;
|
||||
@@ -29,7 +31,7 @@ class DashboardQueryRunnerImpl implements DashboardQueryRunner {
|
||||
private readonly dashboard: DashboardModel,
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv(),
|
||||
private readonly workers: DashboardQueryRunnerWorker[] = [
|
||||
new AlertStatesWorker(),
|
||||
config.featureToggles.ngalert ? new UnifiedAlertStatesWorker() : new AlertStatesWorker(),
|
||||
new SnapshotWorker(),
|
||||
new AnnotationsWorker(),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { AlertState, getDefaultTimeRange, TimeRange } from '@grafana/data';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { DashboardQueryRunnerOptions } from './types';
|
||||
import { UnifiedAlertStatesWorker } from './UnifiedAlertStatesWorker';
|
||||
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
||||
import * as store from '../../../../store/store';
|
||||
import { PromAlertingRuleState, PromRuleDTO, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
function getDefaultOptions(): DashboardQueryRunnerOptions {
|
||||
const dashboard: any = { id: 'an id', uid: 'a uid' };
|
||||
const range = getDefaultTimeRange();
|
||||
|
||||
return { dashboard, range };
|
||||
}
|
||||
|
||||
function getTestContext() {
|
||||
jest.clearAllMocks();
|
||||
const dispatchMock = jest.spyOn(store, 'dispatch');
|
||||
const options = getDefaultOptions();
|
||||
const getMock = jest.spyOn(backendSrv, 'get');
|
||||
|
||||
return { getMock, options, dispatchMock };
|
||||
}
|
||||
|
||||
describe('UnifiedAlertStatesWorker', () => {
|
||||
const worker = new UnifiedAlertStatesWorker();
|
||||
|
||||
describe('when canWork is called with correct props', () => {
|
||||
it('then it should return true', () => {
|
||||
const options = getDefaultOptions();
|
||||
|
||||
expect(worker.canWork(options)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canWork is called with no dashboard id', () => {
|
||||
it('then it should return false', () => {
|
||||
const dashboard: any = {};
|
||||
const options = { ...getDefaultOptions(), dashboard };
|
||||
|
||||
expect(worker.canWork(options)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canWork is called with wrong range', () => {
|
||||
it('then it should return false', () => {
|
||||
const defaultRange = getDefaultTimeRange();
|
||||
const range: TimeRange = { ...defaultRange, raw: { ...defaultRange.raw, to: 'now-6h' } };
|
||||
const options = { ...getDefaultOptions(), range };
|
||||
|
||||
expect(worker.canWork(options)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when run is called with incorrect props', () => {
|
||||
it('then it should return the correct results', async () => {
|
||||
const { getMock, options } = getTestContext();
|
||||
const dashboard: any = {};
|
||||
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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: {
|
||||
groups: [
|
||||
{
|
||||
name: 'group',
|
||||
file: '',
|
||||
interval: 1,
|
||||
rules: [
|
||||
mockPromRuleDTO({
|
||||
state: PromAlertingRuleState.Firing,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: 'a uid',
|
||||
[Annotation.panelID]: '1',
|
||||
},
|
||||
}),
|
||||
mockPromRuleDTO({
|
||||
state: PromAlertingRuleState.Inactive,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: 'a uid',
|
||||
[Annotation.panelID]: '2',
|
||||
},
|
||||
}),
|
||||
mockPromRuleDTO({
|
||||
state: PromAlertingRuleState.Pending,
|
||||
annotations: {
|
||||
[Annotation.dashboardUID]: 'a uid',
|
||||
[Annotation.panelID]: '2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const { getMock, options } = getTestContext();
|
||||
getMock.mockResolvedValue(getResults);
|
||||
|
||||
await expect(worker.work(options)).toEmitValuesWith((received) => {
|
||||
expect(received).toHaveLength(1);
|
||||
const results = received[0];
|
||||
expect(results).toEqual({
|
||||
alertStates: [
|
||||
{ id: 0, state: AlertState.Alerting, dashboardId: 'an id', panelId: 1 },
|
||||
{ id: 1, state: AlertState.Pending, dashboardId: 'an id', panelId: 2 },
|
||||
],
|
||||
annotations: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledWith(
|
||||
'/api/prometheus/grafana/api/v1/rules',
|
||||
{ dashboard_uid: 'a uid' },
|
||||
'dashboard-query-runner-unified-alert-states-an id'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
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 });
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorker, DashboardQueryRunnerWorkerResult } from './types';
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { emptyResult, handleDashboardQueryRunnerWorkerError } from './utils';
|
||||
import { PromAlertingRuleState, PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
import { AlertState, AlertStateInfo } from '@grafana/data';
|
||||
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
|
||||
export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker {
|
||||
// maps dashboard uid to wether it has alert rules.
|
||||
// if it is determined that a dashboard does not have alert rules,
|
||||
// further attempts to get alert states for it will not be made
|
||||
private hasAlertRules: Record<string, boolean> = {};
|
||||
|
||||
canWork({ dashboard, range }: DashboardQueryRunnerOptions): boolean {
|
||||
if (!dashboard.uid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (range.raw.to !== 'now') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.hasAlertRules[dashboard.uid] === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
work(options: DashboardQueryRunnerOptions): Observable<DashboardQueryRunnerWorkerResult> {
|
||||
if (!this.canWork(options)) {
|
||||
return emptyResult();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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.`);
|
||||
}),
|
||||
catchError(handleDashboardQueryRunnerWorkerError)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function promAlertStateToAlertState(state: PromAlertingRuleState): AlertState {
|
||||
if (state === PromAlertingRuleState.Firing) {
|
||||
return AlertState.Alerting;
|
||||
} else if (state === PromAlertingRuleState.Pending) {
|
||||
return AlertState.Pending;
|
||||
}
|
||||
return AlertState.OK;
|
||||
}
|
||||
@@ -52,6 +52,7 @@ export function getDefaultOptions(): DashboardQueryRunnerOptions {
|
||||
subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
|
||||
publish: jest.fn(),
|
||||
},
|
||||
panels: [{ alert: {} } as any],
|
||||
};
|
||||
const range = getDefaultTimeRange();
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface PromAlertingRuleDTO extends PromRuleDTOBase {
|
||||
value: string;
|
||||
}>;
|
||||
labels: Labels;
|
||||
annotations: Annotations;
|
||||
annotations?: Annotations;
|
||||
duration?: number; // for
|
||||
state: PromAlertingRuleState;
|
||||
type: PromRuleType.Alerting;
|
||||
|
||||
Reference in New Issue
Block a user