mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Display a warning when a contact point is not in use (#70506)
This commit is contained in:
parent
ae5b818c6e
commit
9ebede1e18
@ -40,6 +40,7 @@ import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import Receivers from './ContactPoints.v1';
|
||||
|
||||
jest.mock('../../api/alertmanager');
|
||||
jest.mock('../../api/grafana');
|
||||
jest.mock('../../utils/config');
|
||||
@ -540,7 +541,7 @@ describe('Receivers', () => {
|
||||
expect(ui.newContactPointButton.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Contact points state', () => {
|
||||
describe('Contact points health', () => {
|
||||
it('Should render error notifications when there are some points state ', async () => {
|
||||
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
|
||||
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
|
||||
@ -709,5 +710,30 @@ describe('Receivers', () => {
|
||||
expect(receiverRows[1]).toHaveTextContent('critical');
|
||||
expect(receiverRows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Should render "Unused" warning if a contact point is not used in route configuration', async () => {
|
||||
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
|
||||
mocks.api.updateConfig.mockResolvedValue();
|
||||
mocks.api.fetchConfig.mockResolvedValue({
|
||||
...someGrafanaAlertManagerConfig,
|
||||
alertmanager_config: { ...someGrafanaAlertManagerConfig.alertmanager_config, route: { receiver: 'default' } },
|
||||
});
|
||||
|
||||
mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState);
|
||||
renderReceivers();
|
||||
|
||||
await ui.receiversTable.find();
|
||||
//should not render notification error
|
||||
expect(ui.notificationError.query()).not.toBeInTheDocument();
|
||||
//contact points are not expandable
|
||||
expect(ui.contactPointsCollapseToggle.query()).not.toBeInTheDocument();
|
||||
//should render receivers, only one dynamic table
|
||||
let receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
|
||||
|
||||
expect(receiverRows).toHaveLength(2);
|
||||
expect(receiverRows[0]).toHaveTextContent('default');
|
||||
expect(receiverRows[1]).toHaveTextContent('critical');
|
||||
expect(receiverRows[1]).toHaveTextContent('Unused');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -27,6 +27,7 @@ import { ReceiversSection } from './ReceiversSection';
|
||||
import { GrafanaAppBadge } from './grafanaAppReceivers/GrafanaAppBadge';
|
||||
import { useGetReceiversWithGrafanaAppTypes } from './grafanaAppReceivers/grafanaApp';
|
||||
import { ReceiverWithTypes } from './grafanaAppReceivers/types';
|
||||
import { AlertmanagerConfigHealth, useAlertmanagerConfigHealth } from './useAlertmanagerConfigHealth';
|
||||
|
||||
interface UpdateActionProps extends ActionProps {
|
||||
onClickDeleteReceiver: (receiverName: string) => void;
|
||||
@ -57,6 +58,7 @@ function UpdateActions({ permissions, alertManagerName, receiverName, onClickDel
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
permissions: {
|
||||
read: AccessControlAction;
|
||||
@ -80,6 +82,7 @@ function ViewAction({ permissions, alertManagerName, receiverName }: ActionProps
|
||||
</Authorize>
|
||||
);
|
||||
}
|
||||
|
||||
interface ReceiverErrorProps {
|
||||
errorCount: number;
|
||||
errorDetail?: string;
|
||||
@ -146,6 +149,7 @@ const useContactPointsState = (alertManagerName: string) => {
|
||||
const errorStateAvailable = Object.keys(receivers).length > 0;
|
||||
return { contactPointsState, errorStateAvailable };
|
||||
};
|
||||
|
||||
interface ReceiverItem {
|
||||
name: string;
|
||||
types: string[];
|
||||
@ -170,6 +174,7 @@ type NotifierItemTableProps = DynamicTableItemProps<NotifierStatus>;
|
||||
interface NotifiersTableProps {
|
||||
notifiersState: NotifiersState;
|
||||
}
|
||||
|
||||
const isLastNotifyNullDate = (lastNotify: string) => lastNotify === '0001-01-01T00:00:00.000Z';
|
||||
|
||||
function LastNotify({ lastNotifyDate }: { lastNotifyDate: string }) {
|
||||
@ -234,6 +239,7 @@ function NotifiersTable({ notifiersState }: NotifiersTableProps) {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const notifierRows: NotifierItemTableProps[] = Object.entries(notifiersState).flatMap((typeState) =>
|
||||
typeState[1].map((notifierStatus, index) => {
|
||||
return {
|
||||
@ -263,6 +269,7 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
const permissions = getNotificationsPermissions(alertManagerName);
|
||||
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
|
||||
|
||||
const configHealth = useAlertmanagerConfigHealth(config.alertmanager_config);
|
||||
const { contactPointsState, errorStateAvailable } = useContactPointsState(alertManagerName);
|
||||
|
||||
// receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted
|
||||
@ -283,6 +290,7 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
}
|
||||
setReceiverToDelete(undefined);
|
||||
};
|
||||
|
||||
const receivers = useGetReceiversWithGrafanaAppTypes(config.alertmanager_config.receivers ?? []);
|
||||
const rows: RowItemTableProps[] = useMemo(() => {
|
||||
return (
|
||||
@ -309,6 +317,7 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
alertManagerName,
|
||||
errorStateAvailable,
|
||||
contactPointsState,
|
||||
configHealth,
|
||||
onClickDeleteReceiver,
|
||||
permissions,
|
||||
isVanillaAM
|
||||
@ -369,8 +378,12 @@ const errorsByReceiver = (contactPointsState: ContactPointsState, receiverName:
|
||||
|
||||
const someNotifiersWithNoAttempt = (contactPointsState: ContactPointsState, receiverName: string) => {
|
||||
const notifiers = Object.values(contactPointsState?.receivers[receiverName]?.notifiers ?? {});
|
||||
const hasSomeWitNoAttempt =
|
||||
notifiers.length === 0 || notifiers.flat().some((status) => isLastNotifyNullDate(status.lastNotifyAttempt));
|
||||
|
||||
if (notifiers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasSomeWitNoAttempt = notifiers.flat().some((status) => isLastNotifyNullDate(status.lastNotifyAttempt));
|
||||
return hasSomeWitNoAttempt;
|
||||
};
|
||||
|
||||
@ -378,6 +391,7 @@ function useGetColumns(
|
||||
alertManagerName: string,
|
||||
errorStateAvailable: boolean,
|
||||
contactPointsState: ContactPointsState | undefined,
|
||||
configHealth: AlertmanagerConfigHealth,
|
||||
onClickDeleteReceiver: (receiverName: string) => void,
|
||||
permissions: {
|
||||
read: AccessControlAction;
|
||||
@ -388,17 +402,22 @@ function useGetColumns(
|
||||
isVanillaAM: boolean
|
||||
): RowTableColumnProps[] {
|
||||
const tableStyles = useStyles2(getAlertTableStyles);
|
||||
|
||||
const enableHealthColumn =
|
||||
errorStateAvailable || Object.values(configHealth.contactPoints).some((cp) => cp.matchingRoutes === 0);
|
||||
|
||||
const baseColumns: RowTableColumnProps[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Contact point name',
|
||||
renderCell: ({ data: { name, provisioned } }) => (
|
||||
<Stack alignItems="center">
|
||||
<>
|
||||
<div>{name}</div>
|
||||
{provisioned && <ProvisioningBadge />}
|
||||
</Stack>
|
||||
</>
|
||||
),
|
||||
size: 1,
|
||||
size: 3,
|
||||
className: tableStyles.nameCell,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
@ -406,15 +425,20 @@ function useGetColumns(
|
||||
renderCell: ({ data: { types, grafanaAppReceiverType } }) => (
|
||||
<>{grafanaAppReceiverType ? <GrafanaAppBadge grafanaAppType={grafanaAppReceiverType} /> : types.join(', ')}</>
|
||||
),
|
||||
size: 1,
|
||||
size: 2,
|
||||
},
|
||||
];
|
||||
const healthColumn: RowTableColumnProps = {
|
||||
id: 'health',
|
||||
label: 'Health',
|
||||
renderCell: ({ data: { name } }) => {
|
||||
if (configHealth.contactPoints[name]?.matchingRoutes === 0) {
|
||||
return <UnusedContactPointBadge />;
|
||||
}
|
||||
|
||||
return (
|
||||
contactPointsState && (
|
||||
contactPointsState &&
|
||||
Object.entries(contactPointsState.receivers).length > 0 && (
|
||||
<ReceiverHealth
|
||||
errorsByReceiver={errorsByReceiver(contactPointsState, name)}
|
||||
someWithNoAttempt={someNotifiersWithNoAttempt(contactPointsState, name)}
|
||||
@ -422,12 +446,12 @@ function useGetColumns(
|
||||
)
|
||||
);
|
||||
},
|
||||
size: 1,
|
||||
size: '160px',
|
||||
};
|
||||
|
||||
return [
|
||||
...baseColumns,
|
||||
...(errorStateAvailable ? [healthColumn] : []),
|
||||
...(enableHealthColumn ? [healthColumn] : []),
|
||||
{
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
@ -452,3 +476,14 @@ function useGetColumns(
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function UnusedContactPointBadge() {
|
||||
return (
|
||||
<Badge
|
||||
text="Unused"
|
||||
color="orange"
|
||||
icon="exclamation-triangle"
|
||||
tooltip="This contact point is not used in any notification policy and it will not receive any alerts"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
import { countBy } from 'lodash';
|
||||
|
||||
import { AlertmanagerConfig, Route } from '../../../../../plugins/datasource/alertmanager/types';
|
||||
|
||||
export interface ContactPointConfigHealth {
|
||||
matchingRoutes: number;
|
||||
}
|
||||
|
||||
export interface AlertmanagerConfigHealth {
|
||||
contactPoints: Record<string, ContactPointConfigHealth>;
|
||||
}
|
||||
|
||||
export function useAlertmanagerConfigHealth(config: AlertmanagerConfig): AlertmanagerConfigHealth {
|
||||
if (!config.receivers) {
|
||||
return { contactPoints: {} };
|
||||
}
|
||||
|
||||
if (!config.route) {
|
||||
return { contactPoints: Object.fromEntries(config.receivers.map((r) => [r.name, { matchingRoutes: 0 }])) };
|
||||
}
|
||||
|
||||
const definedContactPointNames = config.receivers?.map((receiver) => receiver.name) ?? [];
|
||||
const usedContactPoints = getUsedContactPoints(config.route);
|
||||
const usedContactPointCounts = countBy(usedContactPoints);
|
||||
|
||||
const contactPointsHealth: AlertmanagerConfigHealth['contactPoints'] = {};
|
||||
const configHealth: AlertmanagerConfigHealth = { contactPoints: contactPointsHealth };
|
||||
|
||||
definedContactPointNames.forEach((contactPointName) => {
|
||||
contactPointsHealth[contactPointName] = { matchingRoutes: usedContactPointCounts[contactPointName] ?? 0 };
|
||||
});
|
||||
|
||||
return configHealth;
|
||||
}
|
||||
|
||||
function getUsedContactPoints(route: Route): string[] {
|
||||
const childrenContactPoints = route.routes?.flatMap((route) => getUsedContactPoints(route)) ?? [];
|
||||
if (route.receiver) {
|
||||
return [route.receiver, ...childrenContactPoints];
|
||||
}
|
||||
|
||||
return childrenContactPoints;
|
||||
}
|
@ -18,6 +18,7 @@ import {
|
||||
AlertmanagerStatus,
|
||||
AlertState,
|
||||
GrafanaManagedReceiverConfig,
|
||||
MatcherOperator,
|
||||
Silence,
|
||||
SilenceState,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
@ -387,6 +388,12 @@ export const someGrafanaAlertManagerConfig: AlertManagerCortexConfig = {
|
||||
alertmanager_config: {
|
||||
route: {
|
||||
receiver: 'default',
|
||||
routes: [
|
||||
{
|
||||
receiver: 'critical',
|
||||
object_matchers: [['severity', MatcherOperator.equal, 'critical']],
|
||||
},
|
||||
],
|
||||
},
|
||||
receivers: [
|
||||
{
|
||||
@ -628,6 +635,7 @@ export function getGrafanaRule(override?: Partial<CombinedRule>) {
|
||||
...override,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCloudRule(override?: Partial<CombinedRule>) {
|
||||
return mockCombinedRule({
|
||||
namespace: {
|
||||
|
@ -27,6 +27,9 @@ export const getAlertTableStyles = (theme: GrafanaTheme2) => ({
|
||||
colExpand: css`
|
||||
width: 36px;
|
||||
`,
|
||||
nameCell: css`
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
actionsCell: css`
|
||||
text-align: right;
|
||||
width: 1%;
|
||||
|
Loading…
Reference in New Issue
Block a user