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 React from 'react';
import { render, screen, waitFor, within, userEvent } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector'; import { byRole, byTestId, byText } from 'testing-library-selector';
import { AccessControlAction } from 'app/types/accessControl'; import { AccessControlAction } from 'app/types/accessControl';
import 'core-js/stable/structured-clone'; import 'core-js/stable/structured-clone';
import { TestProvider } from '../../../../../../../test/helpers/TestProvider';
import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types'; import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types';
import { Labels } from '../../../../../../types/unified-alerting-dto'; import { Labels } from '../../../../../../types/unified-alerting-dto';
import { mockApi, setupMswServer } from '../../../mockApi'; import { mockApi, setupMswServer } from '../../../mockApi';
@ -140,9 +138,7 @@ describe('NotificationPreview', () => {
mockOneAlertManager(); mockOneAlertManager();
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, { render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
wrapper: TestProvider,
});
await userEvent.click(ui.previewButton.get()); await userEvent.click(ui.previewButton.get());
await waitFor(() => { await waitFor(() => {
@ -166,9 +162,7 @@ describe('NotificationPreview', () => {
mockTwoAlertManagers(); mockTwoAlertManagers();
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, { render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
wrapper: TestProvider,
});
await waitFor(() => { await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
}); });
@ -195,9 +189,7 @@ describe('NotificationPreview', () => {
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
mockHasEditPermission(true); mockHasEditPermission(true);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, { render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
wrapper: TestProvider,
});
await waitFor(() => { await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
}); });
@ -219,9 +211,7 @@ describe('NotificationPreview', () => {
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
mockHasEditPermission(false); mockHasEditPermission(false);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, { render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />);
wrapper: TestProvider,
});
await waitFor(() => { await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
}); });
@ -266,8 +256,7 @@ describe('NotificationPreviewByAlertmanager', () => {
alertManagerSource={grafanaAlertManagerDataSource} alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances} potentialInstances={potentialInstances}
onlyOneAM={true} onlyOneAM={true}
/>, />
{ wrapper: TestProvider }
); );
await waitFor(() => { await waitFor(() => {
@ -321,8 +310,7 @@ describe('NotificationPreviewByAlertmanager', () => {
alertManagerSource={grafanaAlertManagerDataSource} alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances} potentialInstances={potentialInstances}
onlyOneAM={true} onlyOneAM={true}
/>, />
{ wrapper: TestProvider }
); );
await waitFor(() => { await waitFor(() => {
@ -376,8 +364,7 @@ describe('NotificationPreviewByAlertmanager', () => {
alertManagerSource={grafanaAlertManagerDataSource} alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances} potentialInstances={potentialInstances}
onlyOneAM={true} onlyOneAM={true}
/>, />
{ wrapper: TestProvider }
); );
await waitFor(() => { await waitFor(() => {
@ -402,4 +389,80 @@ describe('NotificationPreviewByAlertmanager', () => {
expect(matchingInstances1).toHaveTextContent(/job=prometheus/); expect(matchingInstances1).toHaveTextContent(/job=prometheus/);
expect(matchingInstances1).toHaveTextContent(/severity=warning/); 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 /> <Spacer />
<Stack gap={2} direction="row" alignItems="center"> <Stack gap={2} direction="row" alignItems="center">
<MetaText icon="layers-alt" data-testid="matching-instances"> <MetaText icon="layers-alt" data-testid="matching-instances">
{instancesCount ?? '-'} {instancesCount ?? '-'} {pluralize('instance', instancesCount)}
<span>{pluralize('instance', instancesCount)}</span>
</MetaText> </MetaText>
<Stack gap={1} direction="row" alignItems="center"> <Stack gap={1} direction="row" alignItems="center">
<div> <div>

View File

@ -476,6 +476,17 @@ describe('matchLabels', () => {
expect(result).toHaveProperty('matches', false); expect(result).toHaveProperty('matches', false);
expect(result.labelsMatch).toMatchSnapshot(); 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', () => { describe('unquoteRouteMatchers', () => {

View File

@ -232,8 +232,17 @@ type OperatorPredicate = (labelValue: string, matcherValue: string) => boolean;
const OperatorFunctions: Record<MatcherOperator, OperatorPredicate> = { const OperatorFunctions: Record<MatcherOperator, OperatorPredicate> = {
[MatcherOperator.equal]: (lv, mv) => lv === mv, [MatcherOperator.equal]: (lv, mv) => lv === mv,
[MatcherOperator.notEqual]: (lv, mv) => lv !== mv, [MatcherOperator.notEqual]: (lv, mv) => lv !== mv,
[MatcherOperator.regex]: (lv, mv) => new RegExp(mv).test(lv), // At the time of writing, Alertmanager compiles to another (anchored) Regular Expression,
[MatcherOperator.notRegex]: (lv, mv) => !new RegExp(mv).test(lv), // 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 { function isLabelMatchInSet(matcher: ObjectMatcher, labels: Label[]): boolean {