Alerting: Add basic support for active_time_intervals (#91710)

This commit is contained in:
Gilles De Mey 2024-08-12 13:51:12 +02:00 committed by GitHub
parent 5487ea444a
commit add99fb3d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 130 additions and 57 deletions

View File

@ -1803,25 +1803,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"]
],
"public/app/features/alerting/unified/components/notification-policies/Policy.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "14"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "15"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "16"]
],
"public/app/features/alerting/unified/components/notification-policies/PromDurationDocs.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],

View File

@ -644,7 +644,7 @@ describe('NotificationPolicies', () => {
renderNotificationPolicies(dataSources.promAlertManager.name);
const rootRouteContainer = await ui.rootRouteContainer.find();
await waitFor(() =>
expect(within(rootRouteContainer).getByTestId('matching-instances')).toHaveTextContent('0instances')
expect(within(rootRouteContainer).getByTestId('matching-instances')).toHaveTextContent('0instance')
);
expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument();

View File

@ -10,7 +10,7 @@ import {
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
import { deleteMuteTimingAction, updateAlertManagerConfigAction } from 'app/features/alerting/unified/state/actions';
import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
import { renameMuteTimings } from 'app/features/alerting/unified/utils/alertmanager';
import { renameTimeInterval } from 'app/features/alerting/unified/utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
import { getK8sNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
@ -261,7 +261,7 @@ export const useUpdateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => {
}
if (nameHasChanged && draft.alertmanager_config.route) {
draft.alertmanager_config.route = renameMuteTimings(
draft.alertmanager_config.route = renameTimeInterval(
timeInterval.name,
originalName,
draft.alertmanager_config.route

View File

@ -100,8 +100,9 @@ describe('Policy', () => {
// for grouping
expect(within(defaultPolicy).getByTestId('grouping')).toHaveTextContent('grafana_folder, alertname');
// no mute timings
// no timings
expect(within(defaultPolicy).queryByTestId('mute-timings')).not.toBeInTheDocument();
expect(within(defaultPolicy).queryByTestId('active-timings')).not.toBeInTheDocument();
// for timing options
expect(within(defaultPolicy).getByTestId('timing-options')).toHaveTextContent(
@ -132,6 +133,7 @@ describe('Policy', () => {
// expect(within(firstPolicy).getByTestId('matching-instances')).toHaveTextContent('0instances');
expect(within(firstPolicy).getByTestId('contact-point')).toHaveTextContent('provisioned-contact-point');
expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted whenmt-1');
expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active whenmt-2');
expect(within(firstPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited2 properties');
// second custom policy should be correct
@ -139,6 +141,7 @@ describe('Policy', () => {
expect(within(secondPolicy).getByTestId('label-matchers')).toHaveTextContent(/^region \= EMEA$/);
expect(within(secondPolicy).queryByTestId('continue-matching')).not.toBeInTheDocument();
expect(within(secondPolicy).queryByTestId('mute-timings')).not.toBeInTheDocument();
expect(within(secondPolicy).queryByTestId('active-timings')).not.toBeInTheDocument();
expect(within(secondPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited3 properties');
// third custom policy should be correct
@ -360,6 +363,7 @@ const mockRoutes: RouteWithID = {
receiver: 'provisioned-contact-point',
object_matchers: [['team', eq, 'operations']],
mute_time_intervals: ['mt-1'],
active_time_intervals: ['mt-2'],
continue: true,
routes: [
{

View File

@ -163,6 +163,7 @@ const Policy = (props: PolicyComponentProps) => {
const groupBy = currentRoute.group_by;
const muteTimings = currentRoute.mute_time_intervals ?? [];
const activeTimings = currentRoute.active_time_intervals ?? [];
const timingOptions: TimingOptions = {
group_wait: currentRoute.group_wait,
@ -245,7 +246,9 @@ const Policy = (props: PolicyComponentProps) => {
) : hasMatchers ? (
<Matchers matchers={matchers ?? []} formatter={getAmMatcherFormatter(alertManagerSourceName)} />
) : (
<span className={styles.metadata}>No matchers</span>
<span className={styles.metadata}>
<Trans i18nKey="alerting.policies.no-matchers">No matchers</Trans>
</span>
)}
<Spacer />
{/* TODO maybe we should move errors to the gutter instead? */}
@ -264,7 +267,7 @@ const Policy = (props: PolicyComponentProps) => {
type="button"
onClick={() => onAddPolicy(currentRoute, 'child')}
>
New child policy
<Trans i18nKey="alerting.policies.new-child">New child policy</Trans>
</Button>
) : (
<Dropdown
@ -296,7 +299,7 @@ const Policy = (props: PolicyComponentProps) => {
icon="angle-down"
type="button"
>
Add new policy
<Trans i18nKey="alerting.policies.new-policy">Add new policy</Trans>
</Button>
</Dropdown>
)}
@ -326,6 +329,7 @@ const Policy = (props: PolicyComponentProps) => {
contactPoint={contactPoint ?? undefined}
groupBy={groupBy}
muteTimings={muteTimings}
activeTimings={activeTimings}
timingOptions={timingOptions}
inheritedProperties={inheritedProperties}
alertManagerSourceName={alertManagerSourceName}
@ -379,7 +383,9 @@ const Policy = (props: PolicyComponentProps) => {
className={styles.moreButtons}
onClick={() => setVisibleChildPolicies(visibleChildPolicies + POLICIES_PER_PAGE)}
>
{moreCount} additional {pluralize('policy', moreCount)}
<Trans i18nKey="alerting.policies.n-more-policies" count={moreCount}>
{{ count: moreCount }} additional policies
</Trans>
</Button>
)}
</>
@ -397,6 +403,7 @@ interface MetadataRowProps {
contactPoint?: string;
groupBy?: string[];
muteTimings?: string[];
activeTimings?: string[];
timingOptions?: TimingOptions;
inheritedProperties?: Partial<InheritableProperties>;
alertManagerSourceName: string;
@ -417,6 +424,7 @@ function MetadataRow({
timingOptions,
groupBy,
muteTimings = [],
activeTimings = [],
matchingInstancesPreview,
inheritedProperties,
matchingAlertGroups,
@ -436,6 +444,7 @@ function MetadataRow({
const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0;
const hasMuteTimings = Boolean(muteTimings.length);
const hasActiveTimings = Boolean(activeTimings.length);
return (
<div className={styles.metadataRow}>
@ -450,12 +459,18 @@ function MetadataRow({
data-testid="matching-instances"
>
<Text color="primary">{numberOfAlertInstances ?? '-'}</Text>
<span>{pluralize('instance', numberOfAlertInstances)}</span>
<span>
<Trans i18nKey="alerting.policies.metadata.n-instances" count={numberOfAlertInstances ?? 0}>
instance
</Trans>
</span>
</MetaText>
)}
{contactPoint && (
<MetaText icon="at" data-testid="contact-point">
<span>Delivered to</span>
<span>
<Trans i18nKey="alerting.policies.metadata.delivered-to">Delivered to</Trans>
</span>
<ContactPointsHoverDetails
alertManagerSourceName={alertManagerSourceName}
receivers={receivers}
@ -467,26 +482,42 @@ function MetadataRow({
<>
{customGrouping && (
<MetaText icon="layer-group" data-testid="grouping">
<span>Grouped by</span>
<span>
<Trans i18nKey="alerting.policies.metadata.grouped-by">Grouped by</Trans>
</span>
<Text color="primary">{groupBy.join(', ')}</Text>
</MetaText>
)}
{singleGroup && (
<MetaText icon="layer-group">
<span>Single group</span>
<span>
<Trans i18nKey="alerting.policies.metadata.grouping.single-group">Single group</Trans>
</span>
</MetaText>
)}
{noGrouping && (
<MetaText icon="layer-group">
<span>Not grouping</span>
<span>
<Trans i18nKey="alerting.policies.metadata.grouping.none">Not grouping</Trans>
</span>
</MetaText>
)}
</>
)}
{hasMuteTimings && (
<MetaText icon="calendar-slash" data-testid="mute-timings">
<span>Muted when</span>
<MuteTimings timings={muteTimings} alertManagerSourceName={alertManagerSourceName} />
<span>
<Trans i18nKey="alerting.policies.metadata.mute-time">Muted when</Trans>
</span>
<TimeIntervals timings={muteTimings} alertManagerSourceName={alertManagerSourceName} />
</MetaText>
)}
{hasActiveTimings && (
<MetaText icon="calendar-alt" data-testid="active-timings">
<span>
<Trans i18nKey="alerting.policies.metadata.active-time">Active when</Trans>
</span>
<TimeIntervals timings={activeTimings} alertManagerSourceName={alertManagerSourceName} />
</MetaText>
)}
{timingOptions && (
@ -498,7 +529,9 @@ function MetadataRow({
{hasInheritedProperties && (
<>
<MetaText icon="corner-down-right-alt" data-testid="inherited-properties">
<span>Inherited</span>
<span>
<Trans i18nKey="alerting.policies.metadata.inherited">Inherited</Trans>
</span>
<InheritedProperties properties={inheritedProperties} />
</MetaText>
</>
@ -514,7 +547,7 @@ export const useCreateDropdownMenuActions = (
provisioned: boolean,
onEditPolicy: (route: RouteWithID, isDefault?: boolean, readOnly?: boolean) => void,
currentRoute: RouteWithID,
toggleShowExportDrawer: (nextValue?: any) => void,
toggleShowExportDrawer: () => void,
onDeletePolicy: (route: RouteWithID) => void
) => {
const [
@ -644,10 +677,12 @@ function DefaultPolicyIndicator() {
return (
<>
<Text element="h2" variant="body" weight="medium">
Default policy
<Trans i18nKey="alerting.policies.default-policy.title">Default policy</Trans>
</Text>
<span className={styles.metadata}>
All alert instances will be handled by the default policy if no other matching policies are found.
<Trans i18nKey="alerting.policies.default-policy.description">
All alert instances will be handled by the default policy if no other matching policies are found.
</Trans>
</span>
</>
);
@ -656,7 +691,7 @@ function DefaultPolicyIndicator() {
function AutogeneratedRootIndicator() {
return (
<Text element="h3" variant="body" weight="medium">
Auto-generated policies
<Trans i18nKey="alerting.policies.generated-policies">Auto-generated policies</Trans>
</Text>
);
}
@ -683,7 +718,7 @@ const InheritedProperties: FC<{ properties: InheritableProperties }> = ({ proper
</HoverCard>
);
const MuteTimings: FC<{ timings: string[]; alertManagerSourceName: string }> = ({
const TimeIntervals: FC<{ timings: string[]; alertManagerSourceName: string }> = ({
timings,
alertManagerSourceName,
}) => {
@ -852,7 +887,9 @@ const ContactPointsHoverDetails: FC<ContactPointDetailsProps> = ({
placement="top"
header={
<MetaText icon="at">
<div>Contact Point</div>
<div>
<Trans i18nKey="alerting.contact-point">Contact Point</Trans>
</div>
<Text color="primary">{contactPoint}</Text>
</MetaText>
}
@ -930,7 +967,7 @@ const routePropertyToValue = (
if (isNotGrouping) {
return (
<Text variant="bodySmall" color="secondary">
Not grouping
<Trans i18nKey="alerting.policies.metadata.grouping.none">Not grouping</Trans>
</Text>
);
}
@ -938,7 +975,7 @@ const routePropertyToValue = (
if (isSingleGroup) {
return (
<Text variant="bodySmall" color="secondary">
Single group
<Trans i18nKey="alerting.policies.metadata.grouping.single-group">Single group</Trans>
</Text>
);
}

View File

@ -52,7 +52,7 @@ import { discoverFeatures } from '../api/buildInfo';
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
import { FetchRulerRulesFilter, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from '../api/ruler';
import { RuleFormValues } from '../types/rule-form';
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
import { addDefaultsToAlertmanagerConfig, removeTimeIntervalFromRoute } from '../utils/alertmanager';
import {
GRAFANA_RULES_SOURCE_NAME,
getAllRulesSourceNames,
@ -590,7 +590,7 @@ export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimin
alertmanager_config: {
...configWithoutMuteTimings,
route: config.alertmanager_config.route
? removeMuteTimingFromRoute(muteTimingName, config.alertmanager_config?.route)
? removeTimeIntervalFromRoute(muteTimingName, config.alertmanager_config?.route)
: undefined,
...time_intervals_without_mute_to_save,
},

View File

@ -1,7 +1,7 @@
import { Matcher, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
import { Labels } from 'app/types/unified-alerting-dto';
import { labelsMatchMatchers, removeMuteTimingFromRoute, matchersToString } from './alertmanager';
import { labelsMatchMatchers, removeTimeIntervalFromRoute, matchersToString } from './alertmanager';
import { parseMatcher, parsePromQLStyleMatcherLooseSafe } from './matchers';
describe('Alertmanager utils', () => {
@ -104,6 +104,7 @@ describe('Alertmanager utils', () => {
receiver: 'slack',
object_matchers: [['env', MatcherOperator.equal, 'prod']],
mute_time_intervals: ['test2'],
active_time_intervals: ['test1'],
},
{
receiver: 'pagerduty',
@ -113,13 +114,15 @@ describe('Alertmanager utils', () => {
],
};
it('should remove mute timings from routes', () => {
expect(removeMuteTimingFromRoute('test1', route)).toEqual({
it('should remove time interval from routes', () => {
expect(removeTimeIntervalFromRoute('test1', route)).toEqual({
mute_time_intervals: ['test2'],
active_time_intervals: [],
object_matchers: [['foo', '=', 'bar']],
receiver: 'gmail',
routes: [
{
active_time_intervals: [],
mute_time_intervals: ['test2'],
object_matchers: [['env', '=', 'prod']],
receiver: 'slack',
@ -127,6 +130,7 @@ describe('Alertmanager utils', () => {
},
{
mute_time_intervals: [],
active_time_intervals: [],
object_matchers: [['env', '=', 'eu']],
receiver: 'pagerduty',
routes: undefined,

View File

@ -35,22 +35,22 @@ export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig
return config;
}
export function removeMuteTimingFromRoute(muteTiming: string, route: Route): Route {
export function removeTimeIntervalFromRoute(muteTiming: string, route: Route): Route {
const newRoute: Route = {
...route,
mute_time_intervals: route.mute_time_intervals?.filter((muteName) => muteName !== muteTiming) ?? [],
routes: route.routes?.map((subRoute) => removeMuteTimingFromRoute(muteTiming, subRoute)),
active_time_intervals: route.active_time_intervals?.filter((muteName) => muteName !== muteTiming) ?? [],
routes: route.routes?.map((subRoute) => removeTimeIntervalFromRoute(muteTiming, subRoute)),
};
return newRoute;
}
export function renameMuteTimings(newMuteTimingName: string, oldMuteTimingName: string, route: Route): Route {
export function renameTimeInterval(newName: string, oldName: string, route: Route): Route {
return {
...route,
mute_time_intervals: route.mute_time_intervals?.map((name) =>
name === oldMuteTimingName ? newMuteTimingName : name
),
routes: route.routes?.map((subRoute) => renameMuteTimings(newMuteTimingName, oldMuteTimingName, subRoute)),
mute_time_intervals: route.mute_time_intervals?.map((name) => (name === oldName ? newName : name)),
active_time_intervals: route.active_time_intervals?.map((name) => (name === oldName ? newName : name)),
routes: route.routes?.map((subRoute) => renameTimeInterval(newName, oldName, subRoute)),
};
}

View File

@ -119,7 +119,10 @@ export type Route = {
group_interval?: string;
repeat_interval?: string;
routes?: Route[];
/** Times when the route should be muted. */
mute_time_intervals?: string[];
/** Times when the route should be active. This is the opposite of `mute_time_intervals` */
active_time_intervals?: string[];
/** only the root policy might have a provenance field defined */
provenance?: string;
};

View File

@ -106,6 +106,7 @@
"export-all": "Export all",
"view": "View"
},
"contact-point": "Contact Point",
"contact-points": {
"delivery-duration": "Last delivery took <1></1>",
"last-delivery-attempt": "Last delivery attempt",
@ -136,7 +137,23 @@
"saving": "Saving mute timing"
},
"policies": {
"default-policy": {
"description": "All alert instances will be handled by the default policy if no other matching policies are found.",
"title": "Default policy"
},
"generated-policies": "Auto-generated policies",
"metadata": {
"active-time": "Active when",
"delivered-to": "Delivered to",
"grouped-by": "Grouped by",
"grouping": {
"none": "Not grouping",
"single-group": "Single group"
},
"inherited": "Inherited",
"mute-time": "Muted when",
"n-instances_one": "instance",
"n-instances_other": "instances",
"timingOptions": {
"groupInterval": {
"description": "How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent.",
@ -151,7 +168,12 @@
"label": "Repeated every <1></1>"
}
}
}
},
"n-more-policies_one": "{{count}} additional policy",
"n-more-policies_other": "{{count}} additional policies",
"new-child": "New child policy",
"new-policy": "Add new policy",
"no-matchers": "No matchers"
},
"provisioning": {
"badge-tooltip-provenance": "This resource has been provisioned via {{provenance}} and cannot be edited through the UI",

View File

@ -106,6 +106,7 @@
"export-all": "Ēχpőřŧ äľľ",
"view": "Vįęŵ"
},
"contact-point": "Cőʼnŧäčŧ Pőįʼnŧ",
"contact-points": {
"delivery-duration": "Ŀäşŧ đęľįvęřy ŧőőĸ <1></1>",
"last-delivery-attempt": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ",
@ -136,7 +137,23 @@
"saving": "Ŝävįʼnģ mūŧę ŧįmįʼnģ"
},
"policies": {
"default-policy": {
"description": "Åľľ äľęřŧ įʼnşŧäʼnčęş ŵįľľ þę ĥäʼnđľęđ þy ŧĥę đęƒäūľŧ pőľįčy įƒ ʼnő őŧĥęř mäŧčĥįʼnģ pőľįčįęş äřę ƒőūʼnđ.",
"title": "Đęƒäūľŧ pőľįčy"
},
"generated-policies": "Åūŧő-ģęʼnęřäŧęđ pőľįčįęş",
"metadata": {
"active-time": "Åčŧįvę ŵĥęʼn",
"delivered-to": "Đęľįvęřęđ ŧő",
"grouped-by": "Ğřőūpęđ þy",
"grouping": {
"none": "Ńőŧ ģřőūpįʼnģ",
"single-group": "Ŝįʼnģľę ģřőūp"
},
"inherited": "Ĩʼnĥęřįŧęđ",
"mute-time": "Mūŧęđ ŵĥęʼn",
"n-instances_one": "įʼnşŧäʼnčę",
"n-instances_other": "įʼnşŧäʼnčęş",
"timingOptions": {
"groupInterval": {
"description": "Ħőŵ ľőʼnģ ŧő ŵäįŧ þęƒőřę şęʼnđįʼnģ ä ʼnőŧįƒįčäŧįőʼn äþőūŧ ʼnęŵ äľęřŧş ŧĥäŧ äřę äđđęđ ŧő ä ģřőūp őƒ äľęřŧş ƒőř ŵĥįčĥ äʼn įʼnįŧįäľ ʼnőŧįƒįčäŧįőʼn ĥäş äľřęäđy þęęʼn şęʼnŧ.",
@ -151,7 +168,12 @@
"label": "Ŗępęäŧęđ ęvęřy <1></1>"
}
}
}
},
"n-more-policies_one": "{{count}} äđđįŧįőʼnäľ pőľįčy",
"n-more-policies_other": "{{count}} äđđįŧįőʼnäľ pőľįčįęş",
"new-child": "Ńęŵ čĥįľđ pőľįčy",
"new-policy": "Åđđ ʼnęŵ pőľįčy",
"no-matchers": "Ńő mäŧčĥęřş"
},
"provisioning": {
"badge-tooltip-provenance": "Ŧĥįş řęşőūřčę ĥäş þęęʼn přővįşįőʼnęđ vįä {{provenance}} äʼnđ čäʼnʼnőŧ þę ęđįŧęđ ŧĥřőūģĥ ŧĥę ŮĨ",