Alerting: refactor fetchAlertManagerConfigAction to use RTK Query (#71261)

This commit is contained in:
Gilles De Mey 2023-07-14 16:53:50 +02:00 committed by GitHub
parent 49b7edfed4
commit fc5d43e1bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 241 additions and 239 deletions

View File

@ -1925,6 +1925,9 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/silences/SilencesFilter.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/alerting/unified/hooks/useControlledFieldArray.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],

View File

@ -5,43 +5,27 @@ import { NavModelItem } from '@grafana/data';
import { Alert } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import MuteTimingForm from './components/mute-timings/MuteTimingForm';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { useAlertmanagerConfig } from './hooks/useAlertmanagerConfig';
import { useAlertmanager } from './state/AlertmanagerContext';
import { fetchAlertManagerConfigAction } from './state/actions';
import { initialAsyncRequestState } from './utils/redux';
const MuteTimings = () => {
const [queryParams] = useQueryParams();
const dispatch = useDispatch();
const { selectedAlertmanager } = useAlertmanager();
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const fetchConfig = useCallback(() => {
if (selectedAlertmanager) {
dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
}
}, [selectedAlertmanager, dispatch]);
useEffect(() => {
fetchConfig();
}, [fetchConfig]);
const { result, error, loading } =
(selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
const config = result?.alertmanager_config;
const { currentData, isLoading, error } = useAlertmanagerConfig(selectedAlertmanager, {
refetchOnFocus: true,
refetchOnReconnect: true,
});
const config = currentData?.alertmanager_config;
const getMuteTimingByName = useCallback(
(id: string): MuteTimeInterval | undefined => {
const timing = config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id);
if (timing) {
const provenance = (config?.muteTimeProvenances ?? {})[timing.name];
const provenance = config?.muteTimeProvenances?.[timing.name];
return {
...timing,
@ -56,15 +40,15 @@ const MuteTimings = () => {
return (
<>
{error && !loading && !result && (
{error && !isLoading && !currentData && (
<Alert severity="error" title={`Error loading Alertmanager config for ${selectedAlertmanager}`}>
{error.message || 'Unknown error.'}
</Alert>
)}
{result && !error && (
{currentData && !error && (
<Switch>
<Route exact path="/alerting/routes/mute-timing/new">
<MuteTimingForm loading={loading} />
<MuteTimingForm loading={isLoading} />
</Route>
<Route exact path="/alerting/routes/mute-timing/edit">
{() => {
@ -74,9 +58,9 @@ const MuteTimings = () => {
return (
<MuteTimingForm
loading={loading}
loading={isLoading}
muteTiming={muteTiming}
showError={!muteTiming && !loading}
showError={!muteTiming && !isLoading}
provenance={provenance}
/>
);

View File

@ -64,7 +64,16 @@ const AmRoutes = () => {
const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? '');
const { result, config, loading: resultLoading, error: resultError } = useAlertmanagerConfig(selectedAlertmanager);
const {
currentData: result,
isLoading: resultLoading,
error: resultError,
} = useAlertmanagerConfig(selectedAlertmanager, {
refetchOnFocus: true,
refetchOnReconnect: true,
});
const config = result?.alertmanager_config;
const { currentData: alertGroups, refetch: refetchAlertGroups } = useGetAlertmanagerAlertGroupsQuery(
{ amSourceName: selectedAlertmanager ?? '' },
@ -187,7 +196,7 @@ const AmRoutes = () => {
const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0;
const haveData = result && !resultError && !resultLoading;
const isLoading = !result && resultLoading;
const isFetching = !result && resultLoading;
const haveError = resultError && !resultLoading;
const muteTimingsTabActive = activeTab === ActiveTab.MuteTimings;
@ -215,7 +224,7 @@ const AmRoutes = () => {
/>
</TabsBar>
<TabContent className={styles.tabContent}>
{isLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{isFetching && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{haveError && (
<Alert severity="error" title="Error loading Alertmanager config">
{resultError.message || 'Unknown error.'}

View File

@ -27,6 +27,6 @@ export const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (
export const alertingApi = createApi({
reducerPath: 'alertingApi',
baseQuery: backendSrvBaseQuery(),
tagTypes: ['AlertmanagerChoice'],
tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration'],
endpoints: () => ({}),
});

View File

@ -1,3 +1,7 @@
import { isEmpty } from 'lodash';
import { dispatch } from 'app/store/store';
import {
AlertmanagerAlert,
AlertmanagerChoice,
@ -8,13 +12,22 @@ import {
ExternalAlertmanagersResponse,
Matcher,
} from '../../../../plugins/datasource/alertmanager/types';
import { withPerformanceLogging } from '../Analytics';
import { matcherToOperator } from '../utils/alertmanager';
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { wrapWithQuotes } from '../utils/misc';
import {
getDatasourceAPIUid,
GRAFANA_RULES_SOURCE_NAME,
isVanillaPrometheusAlertManagerDataSource,
} from '../utils/datasource';
import { retryWhile, wrapWithQuotes } from '../utils/misc';
import { messageFromError, withSerializedError } from '../utils/redux';
import { alertingApi } from './alertingApi';
import { fetchAlertManagerConfig, fetchStatus } from './alertmanager';
import { featureDiscoveryApi } from './featureDiscoveryApi';
const LIMIT_TO_SUCCESSFULLY_APPLIED_AMS = 10;
const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
export interface AlertmanagersChoiceResponse {
alertmanagersChoice: AlertmanagerChoice;
@ -107,5 +120,111 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
method: 'POST',
}),
}),
// TODO we've sort of inherited the errors format here from the previous Redux actions, errors throw are of type "SerializedError"
getAlertmanagerConfiguration: build.query<AlertManagerCortexConfig, string>({
queryFn: async (alertmanagerSourceName: string) => {
const isGrafanaManagedAlertmanager = alertmanagerSourceName === GRAFANA_RULES_SOURCE_NAME;
const isVanillaPrometheusAlertmanager = isVanillaPrometheusAlertManagerDataSource(alertmanagerSourceName);
// for vanilla prometheus, there is no config endpoint. Only fetch config from status
if (isVanillaPrometheusAlertmanager) {
return withSerializedError(
fetchStatus(alertmanagerSourceName).then((status) => ({
data: {
alertmanager_config: status.config,
template_files: {},
},
}))
);
}
// discover features, we want to know if Mimir has "lazyConfigInit" configured
const { data: alertmanagerFeatures } = await dispatch(
featureDiscoveryApi.endpoints.discoverAmFeatures.initiate({
amSourceName: alertmanagerSourceName,
})
);
const defaultConfig = {
alertmanager_config: {},
template_files: {},
template_file_provenances: {},
};
const lazyConfigInitSupported = alertmanagerFeatures?.lazyConfigInit ?? false;
// wrap our fetchConfig function with some performance logging functions
const fetchAMconfigWithLogging = withPerformanceLogging(
fetchAlertManagerConfig,
`[${alertmanagerSourceName}] Alertmanager config loaded`,
{
dataSourceName: alertmanagerSourceName,
thunk: 'unifiedalerting/fetchAmConfig',
}
);
const tryFetchingConfiguration = retryWhile(
() => fetchAMconfigWithLogging(alertmanagerSourceName),
// if config has been recently deleted, it takes a while for cortex start returning the default one.
// retry for a short while instead of failing
(error) =>
!!messageFromError(error)?.includes('alertmanager storage object not found') && !lazyConfigInitSupported,
FETCH_CONFIG_RETRY_TIMEOUT
)
.then((result) => {
if (isGrafanaManagedAlertmanager) {
return result;
}
// if user config is empty for Mimir alertmanager, try to get config from status endpoint
const emptyConfiguration = isEmpty(result.alertmanager_config) && isEmpty(result.template_files);
if (emptyConfiguration) {
return fetchStatus(alertmanagerSourceName).then((status) => ({
alertmanager_config: status.config,
template_files: {},
template_file_provenances: result.template_file_provenances,
last_applied: result.last_applied,
id: result.id,
}));
}
return result;
})
.then((result) => result ?? defaultConfig)
.then((result) => ({ data: result }))
.catch((error) => {
// When mimir doesn't have fallback AM url configured the default response will be as above
// However it's fine, and it's possible to create AM configuration
if (lazyConfigInitSupported && messageFromError(error)?.includes('alertmanager storage object not found')) {
return {
data: defaultConfig,
};
}
throw error;
});
return withSerializedError(tryFetchingConfiguration).catch((err) => ({
error: err,
data: undefined,
}));
},
providesTags: ['AlertmanagerConfiguration'],
}),
updateAlertmanagerConfiguration: build.mutation<
void,
{ selectedAlertmanager: string; config: AlertManagerCortexConfig }
>({
query: ({ selectedAlertmanager, config, ...rest }) => ({
url: `/api/alertmanager/${getDatasourceAPIUid(selectedAlertmanager)}/config/api/v1/alerts`,
method: 'POST',
data: config,
...rest,
}),
invalidatesTags: ['AlertmanagerConfiguration'],
}),
}),
});

View File

@ -132,7 +132,7 @@ describe('Admin config', () => {
await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());
expect(mocks.api.updateAlertManagerConfig.mock.lastCall).toMatchSnapshot();
await waitFor(() => expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3));
await waitFor(() => expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2));
});
it('Read-only when using Prometheus Alertmanager', async () => {

View File

@ -1,19 +1,15 @@
import { css } from '@emotion/css';
import React, { useEffect, useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, useStyles2 } from '@grafana/ui';
import { useDispatch } from 'app/types';
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import {
deleteAlertManagerConfigAction,
fetchAlertManagerConfigAction,
updateAlertManagerConfigAction,
} from '../../state/actions';
import { deleteAlertManagerConfigAction, updateAlertManagerConfigAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import { initialAsyncRequestState } from '../../utils/redux';
import AlertmanagerConfigSelector, { ValidAmConfigOption } from './AlertmanagerConfigSelector';
import { ConfigEditor } from './ConfigEditor';
@ -33,21 +29,13 @@ export default function AlertmanagerConfig(): JSX.Element {
const readOnly = selectedAlertmanager ? isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager) : false;
const styles = useStyles2(getStyles);
const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
const [selectedAmConfig, setSelectedAmConfig] = useState<ValidAmConfigOption | undefined>();
const {
result: config,
loading: isLoadingConfig,
currentData: config,
error: loadingError,
} = (selectedAlertmanager && configRequests[selectedAlertmanager]) || initialAsyncRequestState;
useEffect(() => {
if (selectedAlertmanager) {
dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
}
}, [selectedAlertmanager, dispatch]);
isLoading: isLoadingConfig,
} = useAlertmanagerConfig(selectedAlertmanager);
const resetConfig = () => {
if (selectedAlertmanager) {

View File

@ -306,7 +306,7 @@ describe('Receivers', () => {
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, {
...someGrafanaAlertManagerConfig,
@ -400,7 +400,8 @@ describe('Receivers', () => {
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', {
...someCloudAlertManagerConfig,

View File

@ -1,14 +1,14 @@
import React, { useEffect } from 'react';
import { Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
import { Route, RouteChildrenProps, Switch } from 'react-router-dom';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { useDispatch } from 'app/types';
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from '../../state/actions';
import { fetchGrafanaNotifiersAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { initialAsyncRequestState } from '../../utils/redux';
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { DuplicateTemplateView } from '../receivers/DuplicateTemplateView';
import { EditReceiverView } from '../receivers/EditReceiverView';
@ -25,28 +25,10 @@ export interface NotificationErrorProps {
const Receivers = () => {
const { selectedAlertmanager: alertManagerSourceName } = useAlertmanager();
const dispatch = useDispatch();
const location = useLocation();
const isRoot = location.pathname.endsWith('/alerting/notifications');
const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
const {
result: config,
loading,
error,
} = (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState;
const { currentData: config, isLoading: loading, error } = useAlertmanagerConfig(alertManagerSourceName);
const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
const shouldLoadConfig = isRoot || !config;
// const shouldRenderNotificationStatus = isRoot;
useEffect(() => {
if (alertManagerSourceName && shouldLoadConfig) {
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
}
}, [alertManagerSourceName, dispatch, shouldLoadConfig]);
useEffect(() => {
if (
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&

View File

@ -4,21 +4,16 @@ import { FormProvider, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, Field, FieldSet, Input, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import {
AlertmanagerConfig,
AlertManagerCortexConfig,
MuteTimeInterval,
} from 'app/plugins/datasource/alertmanager/types';
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { updateAlertManagerConfigAction } from '../../state/actions';
import { MuteTimingFields } from '../../types/mute-timing-form';
import { renameMuteTimings } from '../../utils/alertmanager';
import { makeAMLink } from '../../utils/misc';
import { createMuteTiming, defaultTimeInterval } from '../../utils/mute-timings';
import { initialAsyncRequestState } from '../../utils/redux';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
import { MuteTimingTimeInterval } from './MuteTimingTimeInterval';
@ -62,21 +57,22 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
const [updating, setUpdating] = useState(false);
const defaultAmCortexConfig = { alertmanager_config: {}, template_files: {} };
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const { result = defaultAmCortexConfig } =
(selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
const { currentData: result } = useAlertmanagerConfig(selectedAlertmanager);
const config = result?.alertmanager_config;
const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
const defaultValues = useDefaultValues(muteTiming);
const formApi = useForm({ defaultValues });
const onSubmit = (values: MuteTimingFields) => {
if (!result) {
return;
}
const newMuteTiming = createMuteTiming(values);
const muteTimings = muteTiming
? config?.mute_time_intervals?.filter(({ name }) => name !== muteTiming.name)
: config.mute_time_intervals;
: config?.mute_time_intervals;
const newConfig: AlertManagerCortexConfig = {
...result,
@ -84,8 +80,8 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
...config,
route:
muteTiming && newMuteTiming.name !== muteTiming.name
? renameMuteTimings(newMuteTiming.name, muteTiming.name, config.route ?? {})
: config.route,
? renameMuteTimings(newMuteTiming.name, muteTiming.name, config?.route ?? {})
: config?.route,
mute_time_intervals: [...(muteTimings || []), newMuteTiming],
},
};

View File

@ -5,15 +5,14 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { IconButton, LinkButton, Link, useStyles2, ConfirmModal } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types/store';
import { Authorize } from '../../components/Authorize';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { deleteMuteTimingAction } from '../../state/actions';
import { getNotificationsPermissions } from '../../utils/access-control';
import { makeAMLink } from '../../utils/misc';
import { AsyncRequestState, initialAsyncRequestState } from '../../utils/redux';
import { DynamicTable, DynamicTableItemProps, DynamicTableColumnProps } from '../DynamicTable';
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
import { ProvisioningBadge } from '../Provisioning';
@ -31,14 +30,18 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const permissions = getNotificationsPermissions(alertManagerSourceName);
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const { currentData } = useAlertmanagerConfig(alertManagerSourceName, {
refetchOnFocus: true,
refetchOnReconnect: true,
});
const config = currentData?.alertmanager_config;
const [muteTimingName, setMuteTimingName] = useState<string>('');
const { result }: AsyncRequestState<AlertManagerCortexConfig> =
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
const items = useMemo((): Array<DynamicTableItemProps<MuteTimeInterval>> => {
const muteTimings = result?.alertmanager_config?.mute_time_intervals ?? [];
const muteTimingsProvenances = result?.alertmanager_config?.muteTimeProvenances ?? {};
const muteTimings = config?.mute_time_intervals ?? [];
const muteTimingsProvenances = config?.muteTimeProvenances ?? {};
return muteTimings
.filter(({ name }) => (muteTimingNames ? muteTimingNames.includes(name) : true))
@ -51,11 +54,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
},
};
});
}, [
result?.alertmanager_config?.mute_time_intervals,
result?.alertmanager_config?.muteTimeProvenances,
muteTimingNames,
]);
}, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]);
const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName);
@ -100,7 +99,10 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
title="Delete mute timing"
body={`Are you sure you would like to delete "${muteTimingName}"`}
confirmText="Delete"
onConfirm={() => dispatch(deleteMuteTimingAction(alertManagerSourceName, muteTimingName))}
onConfirm={() => {
dispatch(deleteMuteTimingAction(alertManagerSourceName, muteTimingName));
setMuteTimingName('');
}}
onDismiss={() => setMuteTimingName('')}
/>
)}

View File

@ -15,17 +15,14 @@ export const useAlertmanagerNotificationRoutingPreview = (
alertManagerSourceName: string,
potentialInstances: Labels[]
) => {
const {
config: AMConfig,
loading: configLoading,
error: configError,
} = useAlertmanagerConfig(alertManagerSourceName);
const { currentData, isLoading: configLoading, error: configError } = useAlertmanagerConfig(alertManagerSourceName);
const config = currentData?.alertmanager_config;
const { matchInstancesToRoute } = useRouteGroupsMatcher();
// to create the list of matching contact points we need to first get the rootRoute
const { rootRoute, receivers } = useMemo(() => {
if (!AMConfig) {
if (!config) {
return {
receivers: [],
rootRoute: undefined,
@ -33,10 +30,10 @@ export const useAlertmanagerNotificationRoutingPreview = (
}
return {
rootRoute: AMConfig.route ? normalizeRoute(addUniqueIdentifierToRoute(AMConfig.route)) : undefined,
receivers: AMConfig.receivers ?? [],
rootRoute: config.route ? normalizeRoute(addUniqueIdentifierToRoute(config.route)) : undefined,
receivers: config.receivers ?? [],
};
}, [AMConfig]);
}, [config]);
// create maps for routes to be get by id, this map also contains the path to the route
// ⚠️ don't forget to compute the inherited tree before using this map

View File

@ -1,26 +1,23 @@
import { useEffect } from 'react';
import { SerializedError } from '@reduxjs/toolkit';
import { useDispatch } from 'app/types';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { fetchAlertManagerConfigAction } from '../state/actions';
import { initialAsyncRequestState } from '../utils/redux';
type Options = {
refetchOnFocus: boolean;
refetchOnReconnect: boolean;
};
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
// TODO refactor this so we can just call "alertmanagerApi.endpoints.getAlertmanagerConfiguration" everywhere
// and remove this hook since it adds little value
export function useAlertmanagerConfig(amSourceName?: string, options?: Options) {
const fetchConfig = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(amSourceName ?? '', {
...options,
skip: !amSourceName,
});
export function useAlertmanagerConfig(amSourceName?: string) {
const dispatch = useDispatch();
useEffect(() => {
if (amSourceName) {
dispatch(fetchAlertManagerConfigAction(amSourceName));
}
}, [amSourceName, dispatch]);
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const { result, loading, error } = (amSourceName && amConfigs[amSourceName]) || initialAsyncRequestState;
const config = result?.alertmanager_config;
return { result, config, loading, error };
return {
...fetchConfig,
// TODO refactor to get rid of this type assertion
error: fetchConfig.error as SerializedError,
};
}

View File

@ -1,22 +1,18 @@
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { AlertmanagerConfig } from 'app/plugins/datasource/alertmanager/types';
import { useAlertmanager } from '../state/AlertmanagerContext';
import { timeIntervalToString } from '../utils/alertmanager';
import { initialAsyncRequestState } from '../utils/redux';
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
import { useAlertmanagerConfig } from './useAlertmanagerConfig';
export function useMuteTimingOptions(): Array<SelectableValue<string>> {
const { selectedAlertmanager } = useAlertmanager();
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const { currentData } = useAlertmanagerConfig(selectedAlertmanager);
const config = currentData?.alertmanager_config;
return useMemo(() => {
const { result } = (selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
const muteTimingsOptions: Array<SelectableValue<string>> =
config?.mute_time_intervals?.map((value) => ({
value: value.name,
@ -25,5 +21,5 @@ export function useMuteTimingOptions(): Array<SelectableValue<string>> {
})) ?? [];
return muteTimingsOptions;
}, [selectedAlertmanager, amConfigs]);
}, [config]);
}

View File

@ -40,18 +40,16 @@ import {
deleteAlertManagerConfig,
expireSilence,
fetchAlertGroups,
fetchAlertManagerConfig,
fetchAlerts,
fetchExternalAlertmanagerConfig,
fetchExternalAlertmanagers,
fetchSilences,
fetchStatus,
testReceivers,
updateAlertManagerConfig,
} from '../api/alertmanager';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { fetchAnnotations } from '../api/annotations';
import { discoverFeatures } from '../api/buildInfo';
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
import { fetchNotifiers } from '../api/grafana';
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
import {
@ -68,17 +66,14 @@ import {
getRulesDataSource,
getRulesSourceName,
GRAFANA_RULES_SOURCE_NAME,
isVanillaPrometheusAlertManagerDataSource,
} from '../utils/datasource';
import { makeAMLink, retryWhile } from '../utils/misc';
import { AsyncRequestMapSlice, messageFromError, withAppEvents, withSerializedError } from '../utils/redux';
import { makeAMLink } from '../utils/misc';
import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
import * as ruleId from '../utils/rule-id';
import { getRulerClient } from '../utils/rulerClient';
import { getAlertInfo, isRulerNotSupportedResponse } from '../utils/rules';
import { safeParseDurationstr } from '../utils/time';
const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
const dataSources = (getState() as StoreState).unifiedAlerting.dataSources;
const dsConfig = dataSources[rulesSourceName]?.result;
@ -131,76 +126,6 @@ export const fetchPromRulesAction = createAsyncThunk(
}
);
export const fetchAlertManagerConfigAction = createAsyncThunk(
'unifiedalerting/fetchAmConfig',
(alertManagerSourceName: string, thunkAPI): Promise<AlertManagerCortexConfig> =>
withSerializedError(
(async () => {
// for vanilla prometheus, there is no config endpoint. Only fetch config from status
if (isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName)) {
return fetchStatus(alertManagerSourceName).then((status) => ({
alertmanager_config: status.config,
template_files: {},
}));
}
const { data: amFeatures } = await thunkAPI.dispatch(
featureDiscoveryApi.endpoints.discoverAmFeatures.initiate({
amSourceName: alertManagerSourceName,
})
);
const lazyConfigInitSupported = amFeatures?.lazyConfigInit ?? false;
const fetchAMconfigWithLogging = withPerformanceLogging(
fetchAlertManagerConfig,
`[${alertManagerSourceName}] Alertmanager config loaded`,
{
dataSourceName: alertManagerSourceName,
thunk: 'unifiedalerting/fetchAmConfig',
}
);
return retryWhile(
() => fetchAMconfigWithLogging(alertManagerSourceName),
// if config has been recently deleted, it takes a while for cortex start returning the default one.
// retry for a short while instead of failing
(e) => !!messageFromError(e)?.includes('alertmanager storage object not found') && !lazyConfigInitSupported,
FETCH_CONFIG_RETRY_TIMEOUT
)
.then((result) => {
// if user config is empty for cortex alertmanager, try to get config from status endpoint
if (
isEmpty(result.alertmanager_config) &&
isEmpty(result.template_files) &&
alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME
) {
return fetchStatus(alertManagerSourceName).then((status) => ({
alertmanager_config: status.config,
template_files: {},
template_file_provenances: result.template_file_provenances,
last_applied: result.last_applied,
id: result.id,
}));
}
return result;
})
.catch((e) => {
// When mimir doesn't have fallback AM url configured the default response will be as above
// However it's fine, and it's possible to create AM configuration
if (lazyConfigInitSupported && messageFromError(e)?.includes('alertmanager storage object not found')) {
return Promise.resolve<AlertManagerCortexConfig>({
alertmanager_config: {},
template_files: {},
template_file_provenances: {},
});
}
throw e;
});
})()
)
);
export const fetchExternalAlertmanagersAction = createAsyncThunk(
'unifiedAlerting/fetchExternalAlertmanagers',
(): Promise<ExternalAlertmanagersResponse> => {
@ -585,8 +510,9 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
withAppEvents(
withSerializedError(
(async () => {
// TODO there must be a better way here than to dispatch another fetch as this causes re-rendering :(
const latestConfig = await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)).unwrap();
const latestConfig = await thunkAPI
.dispatch(alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName))
.unwrap();
const isLatestConfigEmpty = isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files);
const oldLastConfigsDiffer = JSON.stringify(latestConfig) !== JSON.stringify(oldConfig);
@ -598,7 +524,7 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
}
await updateAlertManagerConfig(alertManagerSourceName, addDefaultsToAlertmanagerConfig(newConfig));
if (refetch) {
await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
thunkAPI.dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
}
if (redirectPath) {
const options = new URLSearchParams(redirectSearch ?? '');
@ -654,8 +580,11 @@ export const createOrUpdateSilenceAction = createAsyncThunk<void, UpdateSilenceA
);
export const deleteReceiverAction = (receiverName: string, alertManagerSourceName: string): ThunkResult<void> => {
return (dispatch, getState) => {
const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
return async (dispatch) => {
const config = await dispatch(
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
).unwrap();
if (!config) {
throw new Error(`Config for ${alertManagerSourceName} not found`);
}
@ -682,8 +611,11 @@ export const deleteReceiverAction = (receiverName: string, alertManagerSourceNam
};
export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult<void> => {
return (dispatch, getState) => {
const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
return async (dispatch) => {
const config = await dispatch(
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
).unwrap();
if (!config) {
throw new Error(`Config for ${alertManagerSourceName} not found`);
}
@ -739,7 +671,7 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
withSerializedError(
(async () => {
await deleteAlertManagerConfig(alertManagerSourceName);
await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
await thunkAPI.dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
})()
),
{
@ -751,8 +683,10 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
);
export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimingName: string): ThunkResult<void> => {
return async (dispatch, getState) => {
const config = getState().unifiedAlerting.amConfigs[alertManagerSourceName].result;
return async (dispatch) => {
const config = await dispatch(
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
).unwrap();
const muteIntervals =
config?.alertmanager_config?.mute_time_intervals?.filter(({ name }) => name !== muteTimingName) ?? [];

View File

@ -6,7 +6,6 @@ import {
createOrUpdateSilenceAction,
deleteAlertManagerConfigAction,
fetchAlertGroupsAction,
fetchAlertManagerConfigAction,
fetchAmAlertsAction,
fetchEditableRuleAction,
fetchExternalAlertmanagersAction,
@ -33,11 +32,6 @@ export const reducer = combineReducers({
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, ({ rulesSourceName }) => rulesSourceName).reducer,
rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, ({ rulesSourceName }) => rulesSourceName)
.reducer,
amConfigs: createAsyncMapSlice(
'amConfigs',
fetchAlertManagerConfigAction,
(alertManagerSourceName) => alertManagerSourceName
).reducer,
silences: createAsyncMapSlice('silences', fetchSilencesAction, (alertManagerSourceName) => alertManagerSourceName)
.reducer,
ruleForm: combineReducers({