mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
2a65a8a1d1
commit
3adb07cf4d
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user