Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alerting: Use time_intervals instead of the deprecated mute_time_intervals in a… #83147

Merged
merged 11 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 155 additions & 4 deletions public/app/features/alerting/unified/MuteTimings.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, waitFor, fireEvent, within } from '@testing-library/react';
import { fireEvent, render, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
Expand All @@ -10,7 +10,7 @@ import { AccessControlAction } from 'app/types';

import MuteTimings from './MuteTimings';
import { fetchAlertManagerConfig, updateAlertManagerConfig } from './api/alertmanager';
import { grantUserPermissions, mockDataSource, MockDataSourceSrv } from './mocks';
import { MockDataSourceSrv, grantUserPermissions, mockDataSource } from './mocks';
import { DataSourceType } from './utils/datasource';

jest.mock('./api/alertmanager');
Expand Down Expand Up @@ -71,6 +71,21 @@ const muteTimeInterval: MuteTimeInterval = {
},
],
};
const muteTimeInterval2: MuteTimeInterval = {
name: 'default-mute2',
time_intervals: [
{
times: [
{
start_time: '12:00',
end_time: '24:00',
},
],
days_of_month: ['15', '-1'],
months: ['august:december', 'march'],
},
],
};

const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
Expand All @@ -90,6 +105,44 @@ const defaultConfig: AlertManagerCortexConfig = {
},
template_files: {},
};
const defaultConfigWithNewTimeIntervalsField: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
time_intervals: [muteTimeInterval],
},
template_files: {},
};

const defaultConfigWithBothTimeIntervalsField: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
time_intervals: [muteTimeInterval],
mute_time_intervals: [muteTimeInterval2],
},
template_files: {},
};

const resetMocks = () => {
jest.resetAllMocks();
Expand All @@ -110,7 +163,102 @@ describe('Mute timings', () => {
grantUserPermissions(Object.values(AccessControlAction));
});

it('creates a new mute timing', async () => {
it('creates a new mute timing, with mute_time_intervals in config', async () => {
renderMuteTimings();

await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
expect(ui.nameField.get()).toBeInTheDocument();

await userEvent.type(ui.nameField.get(), 'maintenance period');
await userEvent.type(ui.startsAt.get(), '22:00');
await userEvent.type(ui.endsAt.get(), '24:00');
await userEvent.type(ui.days.get(), '-1');
await userEvent.type(ui.months.get(), 'january, july');

fireEvent.submit(ui.form.get());

await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());

const { mute_time_intervals: _, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config;
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', {
...defaultConfig,
alertmanager_config: {
...configWithoutMuteTimings,
mute_time_intervals: [
muteTimeInterval,
{
name: 'maintenance period',
time_intervals: [
{
days_of_month: ['-1'],
months: ['january', 'july'],
times: [
{
start_time: '22:00',
end_time: '24:00',
},
],
},
],
},
],
},
});
});

it('creates a new mute timing, with time_intervals in config', async () => {
mocks.api.fetchAlertManagerConfig.mockImplementation(() => {
return Promise.resolve({
...defaultConfigWithNewTimeIntervalsField,
});
});
renderMuteTimings();

await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
expect(ui.nameField.get()).toBeInTheDocument();

await userEvent.type(ui.nameField.get(), 'maintenance period');
await userEvent.type(ui.startsAt.get(), '22:00');
await userEvent.type(ui.endsAt.get(), '24:00');
await userEvent.type(ui.days.get(), '-1');
await userEvent.type(ui.months.get(), 'january, july');

fireEvent.submit(ui.form.get());

await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());

const { mute_time_intervals: _, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config;
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', {
...defaultConfig,
alertmanager_config: {
...configWithoutMuteTimings,
mute_time_intervals: [
muteTimeInterval,
{
name: 'maintenance period',
time_intervals: [
{
days_of_month: ['-1'],
months: ['january', 'july'],
times: [
{
start_time: '22:00',
end_time: '24:00',
},
],
},
],
},
],
},
});
});
it('creates a new mute timing, with time_intervals and mute_time_intervals in config', async () => {
mocks.api.fetchAlertManagerConfig.mockImplementation(() => {
return Promise.resolve({
...defaultConfigWithBothTimeIntervalsField,
});
});
renderMuteTimings();

await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
Expand All @@ -125,12 +273,15 @@ describe('Mute timings', () => {
fireEvent.submit(ui.form.get());

await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());

const { mute_time_intervals, time_intervals, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config;
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', {
...defaultConfig,
alertmanager_config: {
...defaultConfig.alertmanager_config,
...configWithoutMuteTimings,
mute_time_intervals: [
muteTimeInterval,
muteTimeInterval2,
{
name: 'maintenance period',
time_intervals: [
Expand Down
16 changes: 11 additions & 5 deletions public/app/features/alerting/unified/MuteTimings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom';
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';

import { NavModelItem } from '@grafana/data';
import { Alert } from '@grafana/ui';
Expand All @@ -21,8 +21,10 @@ const MuteTimings = () => {
const config = currentData?.alertmanager_config;

const getMuteTimingByName = useCallback(
(id: string): MuteTimeInterval | undefined => {
const timing = config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id);
(id: string, fromTimeIntervals: boolean): MuteTimeInterval | undefined => {
// merge both fields mute_time_intervals and time_intervals to support both old and new config
const time_intervals = fromTimeIntervals ? config?.time_intervals ?? [] : config?.mute_time_intervals ?? [];
const timing = time_intervals?.find(({ name }: MuteTimeInterval) => name === id);
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved

if (timing) {
const provenance = config?.muteTimeProvenances?.[timing.name];
Expand Down Expand Up @@ -53,13 +55,17 @@ const MuteTimings = () => {
<Route exact path="/alerting/routes/mute-timing/edit">
{() => {
if (queryParams['muteName']) {
const muteTiming = getMuteTimingByName(String(queryParams['muteName']));
const muteTimingInMuteTimings = getMuteTimingByName(String(queryParams['muteName']), false);
const muteTimingInTimeIntervals = getMuteTimingByName(String(queryParams['muteName']), true);
const inTimeIntervals = Boolean(muteTimingInTimeIntervals);
const muteTiming = inTimeIntervals ? muteTimingInTimeIntervals : muteTimingInMuteTimings;
const provenance = muteTiming?.provenance;

return (
<MuteTimingForm
loading={isLoading}
muteTiming={muteTiming}
fromMuteTimings={muteTimingInMuteTimings}
fromTimeIntervals={muteTimingInTimeIntervals}
showError={!muteTiming && !isLoading}
provenance={provenance}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,11 @@ const AmRoutes = () => {
if (!selectedAlertmanager) {
return null;
}

const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0;
const time_intervals = [
...(result?.alertmanager_config?.mute_time_intervals ?? []),
...(result?.alertmanager_config?.time_intervals ?? []),
];
const numberOfMuteTimings = time_intervals?.length ?? 0;
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
const haveData = result && !resultError && !resultLoading;
const isFetching = !result && resultLoading;
const haveError = resultError && !resultLoading;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import { useAlertmanager } from '../../state/AlertmanagerContext';
import { updateAlertManagerConfigAction } from '../../state/actions';
import { MuteTimingFields } from '../../types/mute-timing-form';
import { renameMuteTimings } from '../../utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { makeAMLink } from '../../utils/misc';
import { createMuteTiming, defaultTimeInterval } from '../../utils/mute-timings';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';

import { MuteTimingTimeInterval } from './MuteTimingTimeInterval';

interface Props {
muteTiming?: MuteTimeInterval;
fromMuteTimings?: MuteTimeInterval; // mute time interval when comes from the old config , mute_time_intervals
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
fromTimeIntervals?: MuteTimeInterval; // mute time interval when comes from the new config , time_intervals. These two fields are mutually exclusive
showError?: boolean;
provenance?: string;
loading?: boolean;
Expand Down Expand Up @@ -50,7 +52,7 @@ const useDefaultValues = (muteTiming?: MuteTimeInterval): MuteTimingFields => {
};
};

const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) => {
const MuteTimingForm = ({ fromMuteTimings, fromTimeIntervals, showError, loading, provenance }: Props) => {
const dispatch = useDispatch();
const { selectedAlertmanager } = useAlertmanager();
const styles = useStyles2(getStyles);
Expand All @@ -60,6 +62,12 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
const { currentData: result } = useAlertmanagerConfig(selectedAlertmanager);
const config = result?.alertmanager_config;

const fromIntervals = Boolean(fromTimeIntervals);
const muteTiming = fromIntervals ? fromTimeIntervals : fromMuteTimings;

const originalMuteTimings = config?.mute_time_intervals ?? [];
const originalTimeIntervals = config?.time_intervals ?? [];

const defaultValues = useDefaultValues(muteTiming);
const formApi = useForm({ defaultValues });

Expand All @@ -70,19 +78,51 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =

const newMuteTiming = createMuteTiming(values);

const muteTimings = muteTiming
? config?.mute_time_intervals?.filter(({ name }) => name !== muteTiming.name)
: config?.mute_time_intervals;

const originalMuteTimingsWithoutNew = fromMuteTimings
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
? originalMuteTimings?.filter(({ name }) => name !== fromMuteTimings.name)
: originalMuteTimings;
const originalTimeIntervalsWithoutNew = fromTimeIntervals
? originalTimeIntervals?.filter(({ name }) => name !== fromTimeIntervals.name)
: originalTimeIntervals;

const isGrafanaDataSource = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
const isNewMuteTiming = fromTimeIntervals === undefined && fromMuteTimings === undefined;

// If is Grafana data source, we wil save mute timings in the alertmanager_config.mute_time_intervals
// Otherwise, we will save it on alertmanager_config.time_intervals or alertmanager_config.mute_time_intervals depending on the original config

const newMutetimeIntervals = isGrafanaDataSource
? {
// for Grafana data source, we will save mute timings in the alertmanager_config.mute_time_intervals
mute_time_intervals: [
...(originalTimeIntervalsWithoutNew || []),
...(originalMuteTimingsWithoutNew || []),
newMuteTiming,
],
}
: {
// for non-Grafana data source, we will save mute timings in the alertmanager_config.time_intervals or alertmanager_config.mute_time_intervals depending on the original config
time_intervals:
fromTimeIntervals || isNewMuteTiming
? // if fromTimeIntervals and fromMuteTimings are both undefined, meaning it's a new mute timing. Then we will save it in the alertmanager_config.time_intervals
[...originalTimeIntervalsWithoutNew, newMuteTiming]
: [...originalTimeIntervalsWithoutNew],
mute_time_intervals:
!isNewMuteTiming && fromMuteTimings
? [...originalMuteTimingsWithoutNew, newMuteTiming]
: [...originalMuteTimingsWithoutNew],
};

const { mute_time_intervals: _, time_intervals: __, ...configWithoutMuteTimings } = config ?? {};
const newConfig: AlertManagerCortexConfig = {
...result,
alertmanager_config: {
...config,
...configWithoutMuteTimings,
route:
muteTiming && newMuteTiming.name !== muteTiming.name
? renameMuteTimings(newMuteTiming.name, muteTiming.name, config?.route ?? {})
: config?.route,
mute_time_intervals: [...(muteTimings || []), newMuteTiming],
...newMutetimeIntervals,
},
};

Expand Down Expand Up @@ -125,8 +165,13 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
required: true,
validate: (value) => {
if (!muteTiming) {
const existingMuteTiming = config?.mute_time_intervals?.find(({ name }) => value === name);
return existingMuteTiming ? `Mute timing already exists for "${value}"` : true;
const existingMuteTimingInMuteTimings = originalMuteTimings?.find(({ name }) => value === name);
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
const existingMuteTimingInTimeIntervals = originalTimeIntervals?.find(
({ name }) => value === name
);
return existingMuteTimingInMuteTimings || existingMuteTimingInTimeIntervals
? `Mute timing already exists for "${value}"`
: true;
}
return;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
const config = currentData?.alertmanager_config;

const [muteTimingName, setMuteTimingName] = useState<string>('');

const items = useMemo((): Array<DynamicTableItemProps<MuteTimeInterval>> => {
const muteTimings = config?.mute_time_intervals ?? [];
// merge both fields mute_time_intervals and time_intervals to support both old and new config
const time_intervals = [...(config?.mute_time_intervals ?? []), ...(config?.time_intervals ?? [])];
const muteTimings = time_intervals ?? [];
soniaAguilarPeiron marked this conversation as resolved.
Show resolved Hide resolved
const muteTimingsProvenances = config?.muteTimeProvenances ?? {};

return muteTimings
Expand All @@ -88,7 +89,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
},
};
});
}, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]);
}, [config?.muteTimeProvenances, muteTimingNames, config?.mute_time_intervals, config?.time_intervals]);

const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateMuteTiming);

Expand Down
Loading
Loading