Alerting: Make regex notification routing preview consistent with notification policies implementation (#88413)

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Tom Ratcliffe 2024-05-29 17:04:35 +01:00 committed by GitHub
parent 2a65a8a1d1
commit 3adb07cf4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 25 deletions

View File

@ -1,12 +1,10 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { render, screen, waitFor, within, userEvent } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { AccessControlAction } from 'app/types/accessControl';
import 'core-js/stable/structured-clone';
import { TestProvider } from '../../../../../../../test/helpers/TestProvider';
import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types';
import { Labels } from '../../../../../../types/unified-alerting-dto';
import { mockApi, setupMswServer } from '../../../mockApi';
@ -140,9 +138,7 @@ describe('NotificationPreview', () => {
mockOneAlertManager();
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
wrapper: TestProvider,
});
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
await userEvent.click(ui.previewButton.get());
await waitFor(() => {
@ -166,9 +162,7 @@ describe('NotificationPreview', () => {
mockTwoAlertManagers();
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
wrapper: TestProvider,
});
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
});
@ -195,9 +189,7 @@ describe('NotificationPreview', () => {
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
mockHasEditPermission(true);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
wrapper: TestProvider,
});
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
});
@ -219,9 +211,7 @@ describe('NotificationPreview', () => {
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
mockHasEditPermission(false);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
wrapper: TestProvider,
});
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
});
@ -266,8 +256,7 @@ describe('NotificationPreviewByAlertmanager', () => {
alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances}
onlyOneAM={true}
/>,
{ wrapper: TestProvider }
/>
);
await waitFor(() => {
@ -321,8 +310,7 @@ describe('NotificationPreviewByAlertmanager', () => {
alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances}
onlyOneAM={true}
/>,
{ wrapper: TestProvider }
/>
);
await waitFor(() => {
@ -376,8 +364,7 @@ describe('NotificationPreviewByAlertmanager', () => {
alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances}
onlyOneAM={true}
/>,
{ wrapper: TestProvider }
/>
);
await waitFor(() => {
@ -402,4 +389,80 @@ describe('NotificationPreviewByAlertmanager', () => {
expect(matchingInstances1).toHaveTextContent(/job=prometheus/);
expect(matchingInstances1).toHaveTextContent(/severity=warning/);
});
describe('regex matching', () => {
it('does not match regex in middle of the word as alertmanager will anchor when queried via API', async () => {
const potentialInstances: Labels[] = [{ regexfield: 'foobarfoo' }];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
amConfigBuilder
.addReceivers((b) => b.withName('email'))
.withRoute((routeBuilder) =>
routeBuilder
.withReceiver('email')
.addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, 'bar'))
)
);
render(
<NotificationPreviewByAlertManager
alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances}
onlyOneAM={true}
/>
);
expect(await screen.findByText(/default policy/i)).toBeInTheDocument();
expect(screen.queryByText(/regexfield/)).not.toBeInTheDocument();
});
it('matches regex at the start of the word', async () => {
const potentialInstances: Labels[] = [{ regexfield: 'baaaaaaah' }];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
amConfigBuilder
.addReceivers((b) => b.withName('email'))
.withRoute((routeBuilder) =>
routeBuilder
.withReceiver('email')
.addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, 'ba.*h'))
)
);
render(
<NotificationPreviewByAlertManager
alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances}
onlyOneAM={true}
/>
);
expect(await screen.findByText(/regexfield/i)).toBeInTheDocument();
});
it('handles negated regex correctly', async () => {
const potentialInstances: Labels[] = [{ regexfield: 'thing' }];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
amConfigBuilder
.addReceivers((b) => b.withName('email'))
.withRoute((routeBuilder) =>
routeBuilder
.withReceiver('email')
.addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.notRegex, 'thing'))
)
);
render(
<NotificationPreviewByAlertManager
alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances}
onlyOneAM={true}
/>
);
expect(await screen.findByText(/default policy/i)).toBeInTheDocument();
expect(screen.queryByText(/regexfield/i)).not.toBeInTheDocument();
});
});
});

View File

@ -68,8 +68,7 @@ function NotificationRouteHeader({
<Spacer />
<Stack gap={2} direction="row" alignItems="center">
<MetaText icon="layers-alt" data-testid="matching-instances">
{instancesCount ?? '-'}
<span>{pluralize('instance', instancesCount)}</span>
{instancesCount ?? '-'} {pluralize('instance', instancesCount)}
</MetaText>
<Stack gap={1} direction="row" alignItems="center">
<div>

View File

@ -476,6 +476,17 @@ describe('matchLabels', () => {
expect(result).toHaveProperty('matches', false);
expect(result.labelsMatch).toMatchSnapshot();
});
it('does not match unanchored regular expressions', () => {
const result = matchLabels([['foo', MatcherOperator.regex, 'bar']], [['foo', 'barbarbar']]);
// This may seem unintuitive, but this is how Alertmanager matches, as it anchors the regex
expect(result.matches).toEqual(false);
});
it('matches regular expressions with wildcards', () => {
const result = matchLabels([['foo', MatcherOperator.regex, '.*bar.*']], [['foo', 'barbarbar']]);
expect(result.matches).toEqual(true);
});
});
describe('unquoteRouteMatchers', () => {

View File

@ -232,8 +232,17 @@ type OperatorPredicate = (labelValue: string, matcherValue: string) => boolean;
const OperatorFunctions: Record<MatcherOperator, OperatorPredicate> = {
[MatcherOperator.equal]: (lv, mv) => lv === mv,
[MatcherOperator.notEqual]: (lv, mv) => lv !== mv,
[MatcherOperator.regex]: (lv, mv) => new RegExp(mv).test(lv),
[MatcherOperator.notRegex]: (lv, mv) => !new RegExp(mv).test(lv),
// At the time of writing, Alertmanager compiles to another (anchored) Regular Expression,
// so we should also anchor our UI matches for consistency with this behaviour
// https://github.com/prometheus/alertmanager/blob/fd37ce9c95898ca68be1ab4d4529517174b73c33/pkg/labels/matcher.go#L69
[MatcherOperator.regex]: (lv, mv) => {
const re = new RegExp(`^(?:${mv})$`);
return re.test(lv);
},
[MatcherOperator.notRegex]: (lv, mv) => {
const re = new RegExp(`^(?:${mv})$`);
return !re.test(lv);
},
};
function isLabelMatchInSet(matcher: ObjectMatcher, labels: Label[]): boolean {