[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:
committed by
GitHub
parent
55556e911b
commit
3b71eab378
@@ -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 = [
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user