2025-04-01 10:38:02 +09:00

191 lines
7.3 KiB
TypeScript

import { debounce, union, without } from 'lodash';
import { PropsWithChildren, createContext, useContext, useEffect, useRef } from 'react';
import { AppEvents } from '@grafana/data';
import { config, getAppEvents } from '@grafana/runtime';
import { AlertmanagerChoice, GrafanaAlertingConfiguration } from 'app/plugins/datasource/alertmanager/types';
import { dispatch } from 'app/store/store';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { dataSourcesApi } from '../../api/dataSourcesApi';
import {
ExternalAlertmanagerDataSourceWithStatus,
useExternalDataSourceAlertmanagers,
} from '../../hooks/useExternalAmSelector';
import { deleteAlertManagerConfigAction, updateAlertManagerConfigAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME, isAlertmanagerDataSourceInterestedInAlerts } from '../../utils/datasource';
import { isInternalAlertmanagerInterestedInAlerts } from '../../utils/settings';
import { useEnableOrDisableHandlingGrafanaManagedAlerts } from './hooks';
const appEvents = getAppEvents();
interface Context {
configuration?: GrafanaAlertingConfiguration;
externalAlertmanagerDataSourcesWithStatus: ExternalAlertmanagerDataSourceWithStatus[];
isLoading: boolean;
isUpdating: boolean;
// for enabling / disabling Alertmanager datasources as additional receivers
enableAlertmanager: (uid: string) => void;
disableAlertmanager: (uid: string) => void;
// for updating or resetting the configuration for an Alertmanager
updateAlertmanagerSettings: (name: string, oldConfig: string, newConfig: string) => void;
resetAlertmanagerSettings: (name: string) => void;
// this feature toggle is for disabling the "send to external Alertmanagers" feature
forwardingDisabled: boolean;
}
const SettingsContext = createContext<Context | undefined>(undefined);
const isInternalAlertmanager = (uid: string) => uid === GRAFANA_RULES_SOURCE_NAME;
export const SettingsProvider = (props: PropsWithChildren) => {
// this list will keep track of Alertmanager UIDs (including internal) that are interested in receiving alert instances
// this will be used to infer the correct "delivery mode" and update the correct list of datasources with "wantsAlertsReceived"
const interestedAlertmanagers: string[] = [];
const forwardingDisabled = config.featureToggles.alertingDisableSendAlertsExternal === true;
const { currentData: configuration, isLoading: isLoadingConfiguration } =
alertmanagerApi.endpoints.getGrafanaAlertingConfiguration.useQuery();
const [updateConfiguration, updateConfigurationState] =
alertmanagerApi.endpoints.updateGrafanaAlertingConfiguration.useMutation();
const [enableGrafanaManagedAlerts, disableGrafanaManagedAlerts, enableOrDisableHandlingGrafanaManagedAlertsState] =
useEnableOrDisableHandlingGrafanaManagedAlerts();
// we will alwayw refetch because a user could edit a data source and come back to this page
const externalAlertmanagersWithStatus = useExternalDataSourceAlertmanagers({ refetchOnMountOrArgChange: true });
const interestedInternal = isInternalAlertmanagerInterestedInAlerts(configuration);
if (interestedInternal) {
interestedAlertmanagers.push(GRAFANA_RULES_SOURCE_NAME);
}
externalAlertmanagersWithStatus
.filter((dataSource) => isAlertmanagerDataSourceInterestedInAlerts(dataSource.dataSourceSettings))
.forEach((alertmanager) => {
interestedAlertmanagers.push(alertmanager.dataSourceSettings.uid);
});
const enableAlertmanager = (uid: string) => {
const updatedInterestedAlertmanagers = union([uid], interestedAlertmanagers); // union will give us a unique array of uids
const newDeliveryMode = determineDeliveryMode(updatedInterestedAlertmanagers);
if (newDeliveryMode === null) {
return;
}
if (newDeliveryMode !== configuration?.alertmanagersChoice) {
updateConfiguration({ alertmanagersChoice: newDeliveryMode });
}
if (!isInternalAlertmanager(uid)) {
enableGrafanaManagedAlerts(uid);
}
};
const disableAlertmanager = (uid: string) => {
const updatedInterestedAlertmanagers = without(interestedAlertmanagers, uid);
const newDeliveryMode = determineDeliveryMode(updatedInterestedAlertmanagers);
if (newDeliveryMode === null) {
return;
}
if (newDeliveryMode !== configuration?.alertmanagersChoice) {
updateConfiguration({ alertmanagersChoice: newDeliveryMode });
}
if (!isInternalAlertmanager(uid)) {
disableGrafanaManagedAlerts(uid);
}
};
const updateAlertmanagerSettings = (alertManagerName: string, oldConfig: string, newConfig: string): void => {
dispatch(
updateAlertManagerConfigAction({
newConfig: JSON.parse(newConfig),
oldConfig: JSON.parse(oldConfig),
alertManagerSourceName: alertManagerName,
successMessage: 'Alertmanager configuration updated.',
})
);
};
const resetAlertmanagerSettings = (alertmanagerName: string) => {
dispatch(deleteAlertManagerConfigAction(alertmanagerName));
};
const value: Context = {
configuration,
forwardingDisabled,
externalAlertmanagerDataSourcesWithStatus: externalAlertmanagersWithStatus,
enableAlertmanager,
disableAlertmanager,
isLoading: isLoadingConfiguration,
isUpdating: updateConfigurationState.isLoading || enableOrDisableHandlingGrafanaManagedAlertsState.isLoading,
// CRUD for Alertmanager settings
updateAlertmanagerSettings,
resetAlertmanagerSettings,
};
return <SettingsContext.Provider value={value}>{props.children}</SettingsContext.Provider>;
};
function determineDeliveryMode(interestedAlertmanagers: string[]): AlertmanagerChoice | null {
const containsInternalAlertmanager = interestedAlertmanagers.some((uid) => uid === GRAFANA_RULES_SOURCE_NAME);
const containsExternalAlertmanager = interestedAlertmanagers.some((uid) => uid !== GRAFANA_RULES_SOURCE_NAME);
if (containsInternalAlertmanager && containsExternalAlertmanager) {
return AlertmanagerChoice.All;
}
if (!containsInternalAlertmanager && containsExternalAlertmanager) {
return AlertmanagerChoice.External;
}
if (containsInternalAlertmanager && !containsExternalAlertmanager) {
return AlertmanagerChoice.Internal;
}
// if we get here we probably have no targets at all and that's not supposed to be possible.
appEvents.publish({
type: AppEvents.alertError.name,
payload: ['You need to have at least one Alertmanager to receive alerts.'],
});
return null;
}
export function useSettings() {
const context = useContext(SettingsContext);
if (context === undefined) {
throw new Error('useSettings must be used within a SettingsContext');
}
// we'll automatically re-fetch the Alertmanager connection status while any Alertmanagers are pending by invalidating the cache entry
const debouncedUpdateStatus = debounce(() => {
dispatch(dataSourcesApi.util.invalidateTags(['AlertmanagerConnectionStatus']));
}, 3000);
const refetchAlertmanagerConnectionStatus = useRef(debouncedUpdateStatus);
const hasPendingAlertmanagers = context.externalAlertmanagerDataSourcesWithStatus.some(
({ status }) => status === 'pending'
);
if (hasPendingAlertmanagers) {
refetchAlertmanagerConnectionStatus.current();
}
useEffect(() => {
debouncedUpdateStatus.cancel();
}, [debouncedUpdateStatus]);
return context;
}