mirror of
https://github.com/grafana/grafana.git
synced 2025-01-15 11:12:50 -06:00
Alerting: Use alerting API server for contact points list (#91073)
This commit is contained in:
parent
a223c46506
commit
338b318bf4
@ -42,7 +42,12 @@ const ContactPointsTab = () => {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const [queryParams] = useURLSearchParams();
|
||||
|
||||
const { isLoading, error, contactPoints } = useContactPointsWithStatus();
|
||||
const { isLoading, error, contactPoints } = useContactPointsWithStatus({
|
||||
alertmanager: selectedAlertmanager!,
|
||||
fetchPolicies: true,
|
||||
fetchStatuses: true,
|
||||
});
|
||||
|
||||
const { deleteTrigger, updateAlertmanagerState } = useDeleteContactPoint(selectedAlertmanager!);
|
||||
const [addContactPointSupported, addContactPointAllowed] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.CreateContactPoint
|
||||
@ -160,7 +165,9 @@ const ContactPointsPageContents = () => {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const [activeTab, setActiveTab] = useTabQueryParam();
|
||||
|
||||
const { contactPoints } = useContactPointsWithStatus();
|
||||
const { contactPoints } = useContactPointsWithStatus({
|
||||
alertmanager: selectedAlertmanager!,
|
||||
});
|
||||
|
||||
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
|
||||
const showNotificationTemplates = activeTab === ActiveTab.NotificationTemplates;
|
||||
|
@ -2,66 +2,79 @@ import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { getWrapper } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
|
||||
import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
|
||||
import { useContactPointsWithStatus } from './useContactPoints';
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => {
|
||||
const ProviderWrapper = getWrapper({ renderWithRouter: true });
|
||||
return (
|
||||
<ProviderWrapper>
|
||||
<AlertmanagerProvider accessType="notification" alertmanagerSourceName="grafana">
|
||||
{children}
|
||||
</AlertmanagerProvider>
|
||||
</ProviderWrapper>
|
||||
);
|
||||
return <ProviderWrapper>{children}</ProviderWrapper>;
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
|
||||
const getHookResponse = async (featureToggleEnabled: boolean) => {
|
||||
config.featureToggles.alertingApiServer = featureToggleEnabled;
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: true,
|
||||
fetchStatuses: true,
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Only return some properties, as we don't want to compare all
|
||||
// RTK query properties in snapshots/comparison between k8s and non-k8s implementations
|
||||
// (would include properties like requestId, fulfilled, etc.)
|
||||
const { contactPoints, error, isLoading } = result.current;
|
||||
|
||||
return { contactPoints, error, isLoading };
|
||||
};
|
||||
|
||||
describe('useContactPoints', () => {
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
|
||||
setOnCallIntegrations([
|
||||
{
|
||||
display_name: 'grafana-integration',
|
||||
value: 'ABC123',
|
||||
integration_url: 'https://oncall-endpoint.example.com',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return contact points with status', async () => {
|
||||
disablePlugin(SupportedPlugin.OnCall);
|
||||
const snapshot = await getHookResponse(false);
|
||||
expect(snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useContactPointsWithStatus(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
expect(result.current).toMatchSnapshot();
|
||||
it('returns matching responses with and without alertingApiServer', async () => {
|
||||
const snapshotAmConfig = await getHookResponse(false);
|
||||
const snapshotAlertingApiServer = await getHookResponse(true);
|
||||
expect(snapshotAmConfig).toEqual(snapshotAlertingApiServer);
|
||||
});
|
||||
|
||||
describe('when having oncall plugin installed and no alert manager config data', () => {
|
||||
it('should return contact points with oncall metadata', async () => {
|
||||
setOnCallIntegrations([
|
||||
{
|
||||
display_name: 'grafana-integration',
|
||||
value: 'ABC123',
|
||||
integration_url: 'https://oncall-endpoint.example.com',
|
||||
},
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() => useContactPointsWithStatus(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
expect(result.current).toMatchSnapshot();
|
||||
const snapshot = await getHookResponse(false);
|
||||
expect(snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver,
|
||||
generatedReceiversApi,
|
||||
} from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
||||
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
|
||||
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { getNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
@ -18,7 +19,6 @@ import { getNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/uti
|
||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
import { onCallApi } from '../../api/onCallApi';
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
|
||||
import { enhanceContactPointsWithMetadata } from './utils';
|
||||
@ -44,12 +44,17 @@ const {
|
||||
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
|
||||
const { useListNamespacedReceiverQuery } = generatedReceiversApi;
|
||||
|
||||
const defaultOptions = {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if OnCall is installed, and fetch the list of integrations if so.
|
||||
*
|
||||
* Otherwise, returns no data
|
||||
*/
|
||||
const useOnCallIntegrations = ({ skip }: { skip?: boolean } = {}) => {
|
||||
const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
|
||||
const { installed, loading } = usePluginBridge(SupportedPlugin.OnCall);
|
||||
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed });
|
||||
|
||||
@ -84,24 +89,53 @@ const useK8sContactPoints = (...[hookParams, queryOptions]: Parameters<typeof us
|
||||
});
|
||||
};
|
||||
|
||||
const useGetGrafanaContactPoints = () => {
|
||||
/**
|
||||
* Fetch contact points for Grafana Alertmanager, either from the k8s API,
|
||||
* or the `/notifications/receivers` endpoint
|
||||
*/
|
||||
const useFetchGrafanaContactPoints = ({ skip }: Skippable = {}) => {
|
||||
const namespace = getNamespace();
|
||||
const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);
|
||||
const grafanaResponse = useGetContactPointsListQuery(undefined, { skip: useK8sApi });
|
||||
const k8sResponse = useK8sContactPoints({ namespace }, { skip: !useK8sApi });
|
||||
|
||||
const grafanaResponse = useGetContactPointsListQuery(undefined, { skip: skip || useK8sApi });
|
||||
const k8sResponse = useK8sContactPoints({ namespace }, { skip: skip || !useK8sApi });
|
||||
|
||||
return useK8sApi ? k8sResponse : grafanaResponse;
|
||||
};
|
||||
|
||||
type GrafanaFetchOptions = {
|
||||
/**
|
||||
* Should we fetch and include status information about each contact point?
|
||||
*/
|
||||
fetchStatuses?: boolean;
|
||||
/**
|
||||
* Should we fetch and include the number of notification policies that reference each contact point?
|
||||
*/
|
||||
fetchPolicies?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch contact points from separate endpoint (i.e. not the Alertmanager config) and combine with
|
||||
* OnCall integrations and any additional metadata from list of notifiers
|
||||
* (e.g. hydrate with additional names/descriptions)
|
||||
*/
|
||||
export const useGetContactPoints = () => {
|
||||
const onCallResponse = useOnCallIntegrations();
|
||||
const alertNotifiers = useGrafanaNotifiersQuery();
|
||||
const contactPointsListResponse = useGetGrafanaContactPoints();
|
||||
export const useGrafanaContactPoints = ({
|
||||
fetchStatuses,
|
||||
fetchPolicies,
|
||||
skip,
|
||||
}: GrafanaFetchOptions & Skippable = {}) => {
|
||||
const potentiallySkip = { skip };
|
||||
const onCallResponse = useOnCallIntegrations(potentiallySkip);
|
||||
const alertNotifiers = useGrafanaNotifiersQuery(undefined, potentiallySkip);
|
||||
const contactPointsListResponse = useFetchGrafanaContactPoints(potentiallySkip);
|
||||
const contactPointsStatusResponse = useGetContactPointsStatusQuery(undefined, {
|
||||
...defaultOptions,
|
||||
pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
|
||||
skip: skip || !fetchStatuses,
|
||||
});
|
||||
const alertmanagerConfigResponse = useGetAlertmanagerConfigurationQuery(GRAFANA_RULES_SOURCE_NAME, {
|
||||
skip: skip || !fetchPolicies,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
const isLoading = onCallResponse.isLoading || alertNotifiers.isLoading || contactPointsListResponse.isLoading;
|
||||
@ -118,83 +152,55 @@ export const useGetContactPoints = () => {
|
||||
};
|
||||
}
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata(
|
||||
[],
|
||||
alertNotifiers.data,
|
||||
onCallResponse?.data,
|
||||
contactPointsListResponse.data,
|
||||
undefined
|
||||
);
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
status: contactPointsStatusResponse.data,
|
||||
notifiers: alertNotifiers.data,
|
||||
onCallIntegrations: onCallResponse?.data,
|
||||
contactPoints: contactPointsListResponse.data,
|
||||
alertmanagerConfiguration: alertmanagerConfigResponse.data,
|
||||
});
|
||||
|
||||
return {
|
||||
...contactPointsListResponse,
|
||||
contactPoints: enhanced,
|
||||
};
|
||||
}, [
|
||||
alertNotifiers.data,
|
||||
alertNotifiers.isLoading,
|
||||
alertNotifiers,
|
||||
alertmanagerConfigResponse,
|
||||
contactPointsListResponse,
|
||||
onCallResponse?.data,
|
||||
onCallResponse.isLoading,
|
||||
contactPointsStatusResponse,
|
||||
onCallResponse,
|
||||
]);
|
||||
};
|
||||
|
||||
export function useContactPointsWithStatus() {
|
||||
const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager();
|
||||
|
||||
const defaultOptions = {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
};
|
||||
|
||||
// fetch receiver status if we're dealing with a Grafana Managed Alertmanager
|
||||
const fetchContactPointsStatus = useGetContactPointsStatusQuery(undefined, {
|
||||
...defaultOptions,
|
||||
// re-fetch status every so often for up-to-date information
|
||||
pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
|
||||
// skip fetching receiver statuses if not Grafana AM
|
||||
export function useContactPointsWithStatus({
|
||||
alertmanager,
|
||||
fetchStatuses,
|
||||
fetchPolicies,
|
||||
}: GrafanaFetchOptions & BaseAlertmanagerArgs) {
|
||||
const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
const grafanaResponse = useGrafanaContactPoints({
|
||||
skip: !isGrafanaAlertmanager,
|
||||
fetchStatuses,
|
||||
fetchPolicies,
|
||||
});
|
||||
|
||||
// fetch notifier metadata from the Grafana API if we're using a Grafana AM – this will be used to add additional
|
||||
// metadata and canonical names to the receiver
|
||||
const fetchReceiverMetadata = useGrafanaNotifiersQuery(undefined, {
|
||||
skip: !isGrafanaAlertmanager,
|
||||
});
|
||||
|
||||
// if the OnCall plugin is installed, fetch its list of integrations so we can match those to the Grafana Managed contact points
|
||||
const { data: onCallMetadata, isLoading: onCallPluginIntegrationsLoading } = useOnCallIntegrations({
|
||||
skip: !isGrafanaAlertmanager,
|
||||
});
|
||||
|
||||
// fetch the latest config from the Alertmanager
|
||||
// we use this endpoint only when we need to get the number of policies
|
||||
const fetchAlertmanagerConfiguration = useGetAlertmanagerConfigurationQuery(selectedAlertmanager!, {
|
||||
const alertmanagerConfigResponse = useGetAlertmanagerConfigurationQuery(alertmanager, {
|
||||
...defaultOptions,
|
||||
selectFromResult: (result) => ({
|
||||
...result,
|
||||
contactPoints: result.data
|
||||
? enhanceContactPointsWithMetadata(
|
||||
fetchContactPointsStatus.data,
|
||||
isGrafanaAlertmanager ? fetchReceiverMetadata.data : cloudNotifierTypes,
|
||||
onCallMetadata,
|
||||
result.data.alertmanager_config.receivers ?? [],
|
||||
result.data
|
||||
)
|
||||
? enhanceContactPointsWithMetadata({
|
||||
notifiers: cloudNotifierTypes,
|
||||
contactPoints: result.data.alertmanager_config.receivers ?? [],
|
||||
alertmanagerConfiguration: result.data,
|
||||
})
|
||||
: [],
|
||||
}),
|
||||
skip: isGrafanaAlertmanager,
|
||||
});
|
||||
|
||||
// we will fail silently for fetching OnCall plugin status and integrations
|
||||
const error = fetchAlertmanagerConfiguration.error || fetchContactPointsStatus.error;
|
||||
const isLoading =
|
||||
fetchAlertmanagerConfiguration.isLoading || fetchContactPointsStatus.isLoading || onCallPluginIntegrationsLoading;
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading,
|
||||
contactPoints: fetchAlertmanagerConfiguration.contactPoints,
|
||||
};
|
||||
return isGrafanaAlertmanager ? grafanaResponse : alertmanagerConfigResponse;
|
||||
}
|
||||
|
||||
export function useDeleteContactPoint(selectedAlertmanager: string) {
|
||||
|
@ -102,6 +102,14 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
|
||||
grafana_managed_receiver_configs: ReceiverConfigWithMetadata[];
|
||||
}
|
||||
|
||||
type EnhanceContactPointsArgs = {
|
||||
status?: ReceiversStateDTO[];
|
||||
notifiers?: NotifierDTO[];
|
||||
onCallIntegrations?: OnCallIntegrationDTO[] | undefined | null;
|
||||
contactPoints: Receiver[];
|
||||
alertmanagerConfiguration?: AlertManagerCortexConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function adds the status information for each of the integrations (contact point types) in a contact point
|
||||
* 1. we iterate over all contact points
|
||||
@ -110,13 +118,13 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
|
||||
* alertmanagerConfiguration: optional as is passed when we need to get number of policies for each contact point
|
||||
* and we prefer using the data from the read-only endpoint.
|
||||
*/
|
||||
export function enhanceContactPointsWithMetadata(
|
||||
status: ReceiversStateDTO[] = [],
|
||||
notifiers: NotifierDTO[] = [],
|
||||
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
|
||||
contactPoints: Receiver[],
|
||||
alertmanagerConfiguration?: AlertManagerCortexConfig
|
||||
): ContactPointWithMetadata[] {
|
||||
export function enhanceContactPointsWithMetadata({
|
||||
status = [],
|
||||
notifiers = [],
|
||||
onCallIntegrations,
|
||||
contactPoints,
|
||||
alertmanagerConfiguration,
|
||||
}: EnhanceContactPointsArgs): ContactPointWithMetadata[] {
|
||||
// compute the entire inherited tree before finding what notification policies are using a particular contact point
|
||||
const fullyInheritedTree = computeInheritedTree(alertmanagerConfiguration?.alertmanager_config?.route ?? {});
|
||||
const usedContactPoints = getUsedContactPoints(fullyInheritedTree);
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
ReadNamespacedTimeIntervalApiResponse,
|
||||
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
|
||||
import { deleteMuteTimingAction, updateAlertManagerConfigAction } from 'app/features/alerting/unified/state/actions';
|
||||
import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
|
||||
import { renameMuteTimings } from 'app/features/alerting/unified/utils/alertmanager';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||
@ -27,15 +28,6 @@ const {
|
||||
useDeleteNamespacedTimeIntervalMutation,
|
||||
} = timeIntervalsApi;
|
||||
|
||||
type BaseAlertmanagerArgs = {
|
||||
/**
|
||||
* Name of alertmanager being used for mute timings management.
|
||||
*
|
||||
* Hooks will behave differently depending on whether this is `grafana` or an external alertmanager
|
||||
*/
|
||||
alertmanager: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Alertmanager mute time interval, with optional additional metadata
|
||||
* (returned in the case of K8S API implementation)
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select, SelectCommonProps, Text, Stack } from '@grafana/ui';
|
||||
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
|
||||
import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from '../contact-points/constants';
|
||||
import { useContactPointsWithStatus } from '../contact-points/useContactPoints';
|
||||
import { ReceiverConfigWithMetadata } from '../contact-points/utils';
|
||||
|
||||
export const ContactPointSelector = (props: SelectCommonProps<string>) => {
|
||||
const { contactPoints, isLoading, error } = useContactPointsWithStatus();
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const { contactPoints, isLoading, error } = useContactPointsWithStatus({ alertmanager: selectedAlertmanager! });
|
||||
|
||||
// TODO error handling
|
||||
if (error) {
|
||||
|
@ -8,7 +8,7 @@ import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
|
||||
import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoint';
|
||||
import { useGetContactPoints } from '../../../contact-points/useContactPoints';
|
||||
import { useGrafanaContactPoints } from '../../../contact-points/useContactPoints';
|
||||
import { ContactPointWithMetadata } from '../../../contact-points/utils';
|
||||
|
||||
import { ContactPointDetails } from './contactPoint/ContactPointDetails';
|
||||
@ -29,7 +29,7 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo
|
||||
error: errorInContactPointStatus,
|
||||
contactPoints,
|
||||
refetch: refetchReceivers,
|
||||
} = useGetContactPoints();
|
||||
} = useGrafanaContactPoints();
|
||||
|
||||
const [selectedContactPointWithMetadata, setSelectedContactPointWithMetadata] = useState<
|
||||
ContactPointWithMetadata | undefined
|
||||
|
16
public/app/features/alerting/unified/types/hooks.ts
Normal file
16
public/app/features/alerting/unified/types/hooks.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export type BaseAlertmanagerArgs = {
|
||||
/**
|
||||
* Name of alertmanager to use for config entity management
|
||||
*
|
||||
* Hooks will behave differently depending on whether this is `grafana` or an external alertmanager
|
||||
*/
|
||||
alertmanager: string;
|
||||
};
|
||||
|
||||
export type Skippable = {
|
||||
/**
|
||||
* Should we skip requests altogether?
|
||||
* Useful for cases where we want to conditionally call hook methods
|
||||
*/
|
||||
skip?: boolean;
|
||||
};
|
Loading…
Reference in New Issue
Block a user