Alerting: Use new readonly permission endpoints for getting contact points and mute timings (#82132)

* use new read only contact points list endpoint in simplified routing section

* Dont use alertmanager endpoint to get groupby defaults

* Use the new read only endpoint for mute timings in route settings

* review suggestions

* Rename hook

* Use options in params for useContactPointsWithStatus hook

* Refactor useContactPointsWithStatus

* second part of the enhanceContactPointsWithMetadata refactor
This commit is contained in:
Sonia Aguilar 2024-02-09 12:13:37 +01:00 committed by GitHub
parent ed9e26122a
commit 3e93a0991f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 104 additions and 36 deletions

View File

@ -11,7 +11,9 @@ import {
ExternalAlertmanagerConfig,
ExternalAlertmanagers,
ExternalAlertmanagersResponse,
GrafanaManagedContactPoint,
Matcher,
MuteTimeInterval,
} from '../../../../plugins/datasource/alertmanager/types';
import { NotifierDTO } from '../../../../types';
import { withPerformanceLogging } from '../Analytics';
@ -257,5 +259,12 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
}));
},
}),
// Grafana Managed Alertmanager only
getContactPointsList: build.query<GrafanaManagedContactPoint[], void>({
query: () => ({ url: '/api/v1/notifications/receivers' }),
}),
getMuteTimingList: build.query<MuteTimeInterval[], void>({
query: () => ({ url: '/api/v1/notifications/time-intervals' }),
}),
}),
});

View File

@ -27,7 +27,13 @@ const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds
* 3. (if available) additional metadata about Grafana Managed contact points
* 4. (if available) the OnCall plugin metadata
*/
export function useContactPointsWithStatus() {
interface UseContactPointsWithStatusOptions {
includePoliciesCount: boolean;
}
export function useContactPointsWithStatus(
{ includePoliciesCount }: UseContactPointsWithStatusOptions = { includePoliciesCount: true }
) {
const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager();
const { installed: onCallPluginInstalled, loading: onCallPluginStatusLoading } = usePluginBridge(
SupportedPlugin.OnCall
@ -64,6 +70,7 @@ export function useContactPointsWithStatus() {
}
// fetch the latest config from the Alertmanager
// we use this endpoint only when we need to get the number of policies
const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(
selectedAlertmanager!,
{
@ -73,31 +80,56 @@ export function useContactPointsWithStatus() {
...result,
contactPoints: result.data
? enhanceContactPointsWithMetadata(
result.data,
fetchContactPointsStatus.data,
fetchReceiverMetadata.data,
onCallMetadata
onCallMetadata,
result.data.alertmanager_config.receivers ?? [],
result.data
)
: [],
}),
skip: !includePoliciesCount,
}
);
// for Grafana Managed Alertmanager, we use the new read-only endpoint for getting the list of contact points
const fetchGrafanaContactPoints = alertmanagerApi.endpoints.getContactPointsList.useQuery(undefined, {
refetchOnFocus: true,
refetchOnReconnect: true,
selectFromResult: (result) => ({
...result,
contactPoints: result.data
? enhanceContactPointsWithMetadata(
fetchContactPointsStatus.data,
fetchReceiverMetadata.data,
onCallMetadata,
result.data, // contact points from the new readonly endpoint
undefined //no config data
)
: [],
}),
skip: includePoliciesCount || !isGrafanaAlertmanager,
});
// we will fail silently for fetching OnCall plugin status and integrations
const error = fetchAlertmanagerConfiguration.error ?? fetchContactPointsStatus.error;
const error =
fetchAlertmanagerConfiguration.error || fetchGrafanaContactPoints.error || fetchContactPointsStatus.error;
const isLoading =
fetchAlertmanagerConfiguration.isLoading ||
fetchGrafanaContactPoints.isLoading ||
fetchContactPointsStatus.isLoading ||
onCallPluginStatusLoading ||
onCallPluginIntegrationsLoading;
const contactPoints = fetchAlertmanagerConfiguration.contactPoints.sort((a, b) => a.name.localeCompare(b.name));
const unsortedContactPoints = includePoliciesCount
? fetchAlertmanagerConfiguration.contactPoints
: fetchGrafanaContactPoints.contactPoints;
const contactPoints = unsortedContactPoints.sort((a, b) => a.name.localeCompare(b.name));
return {
error,
isLoading,
contactPoints,
refetchReceivers: fetchAlertmanagerConfiguration.refetch,
refetchReceivers: fetchGrafanaContactPoints.refetch,
};
}

View File

@ -7,6 +7,7 @@ import {
GrafanaManagedContactPoint,
GrafanaManagedReceiverConfig,
MatcherOperator,
Receiver,
Route,
} from 'app/plugins/datasource/alertmanager/types';
import { NotifierDTO, NotifierStatus, ReceiversStateDTO } from 'app/types';
@ -30,6 +31,9 @@ export function isProvisioned(contactPoint: GrafanaManagedContactPoint) {
// TODO we should really add some type information to these receiver settings...
export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): ReactNode | undefined {
if (!receiver.settings) {
return undefined;
}
switch (receiver.type) {
case 'email': {
const hasEmailAddresses = 'addresses' in receiver.settings; // when dealing with alertmanager email_configs we don't normalize the settings
@ -87,7 +91,7 @@ export interface ReceiverConfigWithMetadata extends GrafanaManagedReceiverConfig
}
export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
numberOfPolicies: number;
numberOfPolicies?: number; // now is optional as we don't have the data from the read-only endpoint
grafana_managed_receiver_configs: ReceiverConfigWithMetadata[];
}
@ -95,30 +99,36 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
* 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
* 2. for each contact point we "enhance" it with the status or "undefined" for vanilla Alertmanager
* contactPoints: list of contact points
* 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(
result: AlertManagerCortexConfig,
status: ReceiversStateDTO[] = [],
notifiers: NotifierDTO[] = [],
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
contactPoints: Receiver[],
alertmanagerConfiguration?: AlertManagerCortexConfig
): ContactPointWithMetadata[] {
const contactPoints = result.alertmanager_config.receivers ?? [];
// compute the entire inherited tree before finding what notification policies are using a particular contact point
const fullyInheritedTree = computeInheritedTree(result?.alertmanager_config?.route ?? {});
const fullyInheritedTree = computeInheritedTree(alertmanagerConfiguration?.alertmanager_config?.route ?? {});
const usedContactPoints = getUsedContactPoints(fullyInheritedTree);
const usedContactPointsByName = countBy(usedContactPoints);
return contactPoints.map((contactPoint) => {
const contactPointsList = alertmanagerConfiguration
? alertmanagerConfiguration?.alertmanager_config.receivers ?? []
: contactPoints ?? [];
return contactPointsList.map((contactPoint) => {
const receivers = extractReceivers(contactPoint);
const statusForReceiver = status.find((status) => status.name === contactPoint.name);
return {
...contactPoint,
numberOfPolicies: usedContactPointsByName[contactPoint.name] ?? 0,
numberOfPolicies:
alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? 0),
grafana_managed_receiver_configs: receivers.map((receiver, index) => {
const isOnCallReceiver = receiver.type === ReceiverTypes.OnCall;
return {
...receiver,
[RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index],
@ -130,6 +140,7 @@ export function enhanceContactPointsWithMetadata(
};
});
}
export function isAutoGeneratedPolicy(route: Route) {
const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;
if (!simplifiedRoutingToggleEnabled) {

View File

@ -22,7 +22,12 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo
const styles = useStyles2(getStyles);
const alertManagerName = alertManager.name;
const { isLoading, error: errorInContactPointStatus, contactPoints, refetchReceivers } = useContactPointsWithStatus();
const {
isLoading,
error: errorInContactPointStatus,
contactPoints,
refetchReceivers,
} = useContactPointsWithStatus({ includePoliciesCount: false });
const [selectedContactPointWithMetadata, setSelectedContactPointWithMetadata] = useState<
ContactPointWithMetadata | undefined
>();

View File

@ -1,9 +1,11 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { SelectableValue } from '@grafana/data';
import { Field, InputControl, MultiSelect, useStyles2 } from '@grafana/ui';
import { useMuteTimingOptions } from 'app/features/alerting/unified/hooks/useMuteTimingOptions';
import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { timeIntervalToString } from 'app/features/alerting/unified/utils/alertmanager';
import { mapMultiSelectValueToStrings } from 'app/features/alerting/unified/utils/amroutes';
import { getFormStyles } from '../../../../notification-policies/formStyles';
@ -19,7 +21,7 @@ export function MuteTimingFields({ alertManager }: MuteTimingFieldsProps) {
formState: { errors },
} = useFormContext<RuleFormValues>();
const muteTimingOptions = useMuteTimingOptions();
const muteTimingOptions = useSelectableMuteTimings();
return (
<Field
label="Mute timings"
@ -44,3 +46,21 @@ export function MuteTimingFields({ alertManager }: MuteTimingFieldsProps) {
</Field>
);
}
function useSelectableMuteTimings(): Array<SelectableValue<string>> {
const fetchGrafanaMuteTimings = alertmanagerApi.endpoints.getMuteTimingList.useQuery(undefined, {
refetchOnFocus: true,
refetchOnReconnect: true,
selectFromResult: (result) => ({
...result,
mutetimings: result.data
? result.data.map((value) => ({
value: value.name,
label: value.name,
description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '),
}))
: [],
}),
});
return fetchGrafanaMuteTimings.mutetimings;
}

View File

@ -14,8 +14,6 @@ import {
Text,
useStyles2,
} from '@grafana/ui';
import { useAlertmanagerConfig } from 'app/features/alerting/unified/hooks/useAlertmanagerConfig';
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import {
commonGroupByOptions,
@ -42,7 +40,7 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => {
formState: { errors },
} = useFormContext<RuleFormValues>();
const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues([]));
const { groupBy, groupIntervalValue, groupWaitValue, repeatIntervalValue } = useGetDefaultsForRoutingSettings();
const { groupIntervalValue, groupWaitValue, repeatIntervalValue } = getDefaultsForRoutingSettings();
const overrideGrouping = watch(`contactPoints.${alertManager}.overrideGrouping`);
const overrideTimings = watch(`contactPoints.${alertManager}.overrideTimings`);
const requiredFieldsInGroupBy = ['grafana_folder', 'alertname'];
@ -56,7 +54,7 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => {
</InlineField>
{!overrideGrouping && (
<Text variant="body" color="secondary">
Grouping: <strong>{groupBy.join(', ')}</strong>
Grouping: <strong>{requiredFieldsInGroupBy.join(', ')}</strong>
</Text>
)}
</Stack>
@ -131,20 +129,13 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => {
);
};
function useGetDefaultsForRoutingSettings() {
const { selectedAlertmanager } = useAlertmanager();
const { currentData } = useAlertmanagerConfig(selectedAlertmanager);
const config = currentData?.alertmanager_config;
return React.useMemo(() => {
return {
groupWaitValue: TIMING_OPTIONS_DEFAULTS.group_wait,
groupIntervalValue: TIMING_OPTIONS_DEFAULTS.group_interval,
repeatIntervalValue: TIMING_OPTIONS_DEFAULTS.repeat_interval,
groupBy: config?.route?.group_by ?? [],
};
}, [config]);
function getDefaultsForRoutingSettings() {
return {
groupWaitValue: TIMING_OPTIONS_DEFAULTS.group_wait,
groupIntervalValue: TIMING_OPTIONS_DEFAULTS.group_interval,
repeatIntervalValue: TIMING_OPTIONS_DEFAULTS.repeat_interval,
};
}
const getStyles = (theme: GrafanaTheme2) => ({
switchElement: css({
flexFlow: 'row-reverse',