[v11.0.x] Alerting: Take receivers into account when custom grouping Alertmanager groups (#86699)

Alerting: Take receivers into account when custom grouping Alertmanager groups (#86127)

* Take receiver into account when custom grouping Alertmanager alert groups

* Fix and add tests

(cherry picked from commit acd3e83c1c)

Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
grafana-delivery-bot[bot]
2024-04-22 16:39:32 +02:00
committed by GitHub
parent 55556e911b
commit 3b71eab378
3 changed files with 79 additions and 34 deletions

View File

@@ -1,4 +1,4 @@
import { render, waitFor } from '@testing-library/react';
import { render, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
@@ -56,6 +56,7 @@ const ui = {
groupByContainer: byTestId('group-by-container'),
groupByInput: byRole('combobox', { name: /group by label keys/i }),
clearButton: byRole('button', { name: 'Clear filters' }),
loadingIndicator: byText('Loading notifications'),
};
describe('AlertGroups', () => {
@@ -66,20 +67,24 @@ describe('AlertGroups', () => {
AccessControlAction.AlertingInstancesExternalRead,
AccessControlAction.AlertingRuleRead,
]);
mocks.api.fetchAlertGroups.mockImplementation(() => {
return Promise.resolve([
mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }),
mockAlertGroup(),
]);
});
});
beforeEach(() => {
setDataSourceSrv(new MockDataSourceSrv(dataSources));
});
afterEach(() => {
mocks.api.fetchAlertGroups.mockClear();
});
it('loads and shows groups', async () => {
mocks.api.fetchAlertGroups.mockImplementation(() => {
return Promise.resolve([
mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }),
mockAlertGroup(),
]);
});
renderAmNotifications();
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled());
@@ -105,9 +110,12 @@ describe('AlertGroups', () => {
mockAlertGroup({
labels: { region },
alerts: [
mockAlertmanagerAlert({ labels: { region, appName: 'billing', env: 'production' } }),
mockAlertmanagerAlert({ labels: { region, appName: 'auth', env: 'staging', uniqueLabel: 'true' } }),
mockAlertmanagerAlert({ labels: { region, appName: 'frontend', env: 'production' } }),
mockAlertmanagerAlert({ fingerprint: '1', labels: { region, appName: 'billing', env: 'production' } }),
mockAlertmanagerAlert({
fingerprint: '2',
labels: { region, appName: 'auth', env: 'staging', uniqueLabel: 'true' },
}),
mockAlertmanagerAlert({ fingerprint: '3', labels: { region, appName: 'frontend', env: 'production' } }),
],
})
);
@@ -161,6 +169,33 @@ describe('AlertGroups', () => {
expect(groups[1]).toHaveTextContent('uniqueLabeltrue');
});
it('should split custom grouping groups with the same label by receiver', async () => {
// The same alert is repeated in two groups with different receivers
const alert = mockAlertmanagerAlert({
fingerprint: '1',
labels: { region: 'NASA', appName: 'billing' },
receivers: [{ name: 'slack' }, { name: 'email' }],
});
const amGroups = [
mockAlertGroup({ receiver: { name: 'slack' }, labels: { region: 'NASA' }, alerts: [alert] }),
mockAlertGroup({ receiver: { name: 'email' }, labels: { region: 'NASA' }, alerts: [alert] }),
];
mocks.api.fetchAlertGroups.mockResolvedValue(amGroups);
const user = userEvent.setup();
renderAmNotifications();
await waitForElementToBeRemoved(ui.loadingIndicator.query());
await user.type(ui.groupByInput.get(), 'appName{enter}');
const groups = await ui.group.findAll();
expect(groups).toHaveLength(2);
expect(groups[0]).toHaveTextContent('appNamebillingDelivered to slack');
expect(groups[1]).toHaveTextContent('appNamebillingDelivered to email');
});
it('should combine multiple ungrouped groups', async () => {
mocks.api.fetchAlertGroups.mockImplementation(() => {
const groups = [

View File

@@ -27,31 +27,45 @@ export const useGroupedAlerts = (groups: AlertmanagerGroup[], groupBy: string[])
return groups;
}
}
const alerts = groups.flatMap(({ alerts }) => alerts);
// api/v2/alerts/groups returns alerts grouped by labels AND receiver.
// It means that the same alert can be in multiple groups if it has multiple receivers.
// Hence, to get the list of unique alerts we need to get unique alerts by fingerprint.
const alerts = uniqBy(
groups.flatMap(({ alerts }) => alerts),
(alert) => alert.fingerprint
);
return alerts.reduce<AlertmanagerGroup[]>((groupings, alert) => {
const alertContainsGroupings = groupBy.every((groupByLabel) => Object.keys(alert.labels).includes(groupByLabel));
if (alertContainsGroupings) {
const existingGrouping = groupings.find((group) => {
return groupBy.every((groupKey) => {
return group.labels[groupKey] === alert.labels[groupKey];
});
});
if (!existingGrouping) {
const labels = groupBy.reduce<Labels>((acc, key) => {
// We need to create a group for each receiver. This is how Alertmanager groups alerts.
// Alertmanager not only does grouping by labels but also by receiver.
const receiverAlertGroups = alert.receivers.map<AlertmanagerGroup>((receiver) => ({
alerts: [alert],
labels: groupBy.reduce<Labels>((acc, key) => {
acc = { ...acc, [key]: alert.labels[key] };
return acc;
}, {});
groupings.push({
alerts: [alert],
labels,
receiver: {
name: 'NONE',
},
}, {}),
receiver,
}));
// Merge the same groupings - groupings are the same if they have the same labels and receiver
receiverAlertGroups.forEach((receiverAlertGroup) => {
const existingGroup = groupings.find((grouping) => {
return (
Object.keys(receiverAlertGroup.labels).every(
(key) => grouping.labels[key] === receiverAlertGroup.labels[key]
) && grouping.receiver.name === receiverAlertGroup.receiver.name
);
});
} else {
existingGrouping.alerts.push(alert);
}
if (existingGroup) {
existingGroup.alerts.push(alert);
} else {
groupings.push(receiverAlertGroup);
}
});
} else {
const noGroupingGroup = groupings.find((group) => Object.keys(group.labels).length === 0);
if (!noGroupingGroup) {

View File

@@ -218,11 +218,7 @@ export type AlertmanagerAlert = {
generatorURL?: string;
labels: { [key: string]: string };
annotations: { [key: string]: string };
receivers: [
{
name: string;
},
];
receivers: Array<{ name: string }>;
fingerprint: string;
status: {
state: AlertState;