Alerting: Use alerting API server for contact points list (#91073)

This commit is contained in:
Tom Ratcliffe 2024-08-05 14:01:48 +01:00 committed by GitHub
parent a223c46506
commit 338b318bf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 163 additions and 119 deletions

View File

@ -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;

View File

@ -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();
});
});
});

View File

@ -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) {

View File

@ -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);

View File

@ -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)

View File

@ -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) {

View File

@ -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

View 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;
};