[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:
co-authored by
Konrad Lalik
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 userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
@@ -56,6 +56,7 @@ const ui = {
|
|||||||
groupByContainer: byTestId('group-by-container'),
|
groupByContainer: byTestId('group-by-container'),
|
||||||
groupByInput: byRole('combobox', { name: /group by label keys/i }),
|
groupByInput: byRole('combobox', { name: /group by label keys/i }),
|
||||||
clearButton: byRole('button', { name: 'Clear filters' }),
|
clearButton: byRole('button', { name: 'Clear filters' }),
|
||||||
|
loadingIndicator: byText('Loading notifications'),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AlertGroups', () => {
|
describe('AlertGroups', () => {
|
||||||
@@ -66,20 +67,24 @@ describe('AlertGroups', () => {
|
|||||||
AccessControlAction.AlertingInstancesExternalRead,
|
AccessControlAction.AlertingInstancesExternalRead,
|
||||||
AccessControlAction.AlertingRuleRead,
|
AccessControlAction.AlertingRuleRead,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mocks.api.fetchAlertGroups.mockImplementation(() => {
|
|
||||||
return Promise.resolve([
|
|
||||||
mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }),
|
|
||||||
mockAlertGroup(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mocks.api.fetchAlertGroups.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
it('loads and shows groups', async () => {
|
it('loads and shows groups', async () => {
|
||||||
|
mocks.api.fetchAlertGroups.mockImplementation(() => {
|
||||||
|
return Promise.resolve([
|
||||||
|
mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }),
|
||||||
|
mockAlertGroup(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
renderAmNotifications();
|
renderAmNotifications();
|
||||||
|
|
||||||
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled());
|
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled());
|
||||||
@@ -105,9 +110,12 @@ describe('AlertGroups', () => {
|
|||||||
mockAlertGroup({
|
mockAlertGroup({
|
||||||
labels: { region },
|
labels: { region },
|
||||||
alerts: [
|
alerts: [
|
||||||
mockAlertmanagerAlert({ labels: { region, appName: 'billing', env: 'production' } }),
|
mockAlertmanagerAlert({ fingerprint: '1', labels: { region, appName: 'billing', env: 'production' } }),
|
||||||
mockAlertmanagerAlert({ labels: { region, appName: 'auth', env: 'staging', uniqueLabel: 'true' } }),
|
mockAlertmanagerAlert({
|
||||||
mockAlertmanagerAlert({ labels: { region, appName: 'frontend', env: 'production' } }),
|
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');
|
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 () => {
|
it('should combine multiple ungrouped groups', async () => {
|
||||||
mocks.api.fetchAlertGroups.mockImplementation(() => {
|
mocks.api.fetchAlertGroups.mockImplementation(() => {
|
||||||
const groups = [
|
const groups = [
|
||||||
|
|||||||
@@ -27,31 +27,45 @@ export const useGroupedAlerts = (groups: AlertmanagerGroup[], groupBy: string[])
|
|||||||
return groups;
|
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) => {
|
return alerts.reduce<AlertmanagerGroup[]>((groupings, alert) => {
|
||||||
const alertContainsGroupings = groupBy.every((groupByLabel) => Object.keys(alert.labels).includes(groupByLabel));
|
const alertContainsGroupings = groupBy.every((groupByLabel) => Object.keys(alert.labels).includes(groupByLabel));
|
||||||
|
|
||||||
if (alertContainsGroupings) {
|
if (alertContainsGroupings) {
|
||||||
const existingGrouping = groupings.find((group) => {
|
// We need to create a group for each receiver. This is how Alertmanager groups alerts.
|
||||||
return groupBy.every((groupKey) => {
|
// Alertmanager not only does grouping by labels but also by receiver.
|
||||||
return group.labels[groupKey] === alert.labels[groupKey];
|
const receiverAlertGroups = alert.receivers.map<AlertmanagerGroup>((receiver) => ({
|
||||||
});
|
alerts: [alert],
|
||||||
});
|
labels: groupBy.reduce<Labels>((acc, key) => {
|
||||||
if (!existingGrouping) {
|
|
||||||
const labels = groupBy.reduce<Labels>((acc, key) => {
|
|
||||||
acc = { ...acc, [key]: alert.labels[key] };
|
acc = { ...acc, [key]: alert.labels[key] };
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {}),
|
||||||
groupings.push({
|
receiver,
|
||||||
alerts: [alert],
|
}));
|
||||||
labels,
|
|
||||||
receiver: {
|
// Merge the same groupings - groupings are the same if they have the same labels and receiver
|
||||||
name: 'NONE',
|
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 {
|
} else {
|
||||||
const noGroupingGroup = groupings.find((group) => Object.keys(group.labels).length === 0);
|
const noGroupingGroup = groupings.find((group) => Object.keys(group.labels).length === 0);
|
||||||
if (!noGroupingGroup) {
|
if (!noGroupingGroup) {
|
||||||
|
|||||||
@@ -218,11 +218,7 @@ export type AlertmanagerAlert = {
|
|||||||
generatorURL?: string;
|
generatorURL?: string;
|
||||||
labels: { [key: string]: string };
|
labels: { [key: string]: string };
|
||||||
annotations: { [key: string]: string };
|
annotations: { [key: string]: string };
|
||||||
receivers: [
|
receivers: Array<{ name: string }>;
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
fingerprint: string;
|
fingerprint: string;
|
||||||
status: {
|
status: {
|
||||||
state: AlertState;
|
state: AlertState;
|
||||||
|
|||||||
Reference in New Issue
Block a user