Alerting: fix route, silence matchers (#34372)

pull/34304/head
Domas 4 years ago committed by GitHub
parent 97cf00ab33
commit 538b1b196b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      public/app/features/alerting/unified/AmRoutes.test.tsx
  2. 13
      public/app/features/alerting/unified/AmRoutes.tsx
  3. 4
      public/app/features/alerting/unified/api/alertmanager.ts
  4. 7
      public/app/features/alerting/unified/components/AlertLabel.tsx
  5. 33
      public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx
  6. 14
      public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx
  7. 2
      public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx
  8. 10
      public/app/features/alerting/unified/components/silences/Matchers.tsx
  9. 46
      public/app/features/alerting/unified/components/silences/MatchersField.tsx
  10. 2
      public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
  11. 2
      public/app/features/alerting/unified/state/actions.ts
  12. 9
      public/app/features/alerting/unified/types/amroutes.ts
  13. 4
      public/app/features/alerting/unified/types/silence-form.ts
  14. 30
      public/app/features/alerting/unified/utils/alertmanager-config.ts
  15. 69
      public/app/features/alerting/unified/utils/alertmanager.test.ts
  16. 95
      public/app/features/alerting/unified/utils/alertmanager.ts
  17. 94
      public/app/features/alerting/unified/utils/amroutes.ts
  18. 15
      public/app/plugins/datasource/alertmanager/types.ts

@ -180,13 +180,14 @@ describe('AmRoutes', () => {
expect(rows).toHaveLength(2);
subroutes.forEach((route, index) => {
Object.entries({
...(route.match ?? {}),
...(route.match_re ?? {}),
}).forEach(([label, value]) => {
Object.entries(route.match ?? {}).forEach(([label, value]) => {
expect(rows[index]).toHaveTextContent(`${label}=${value}`);
});
Object.entries(route.match_re ?? {}).forEach(([label, value]) => {
expect(rows[index]).toHaveTextContent(`${label}=~${value}`);
});
if (route.group_by) {
expect(rows[index]).toHaveTextContent(route.group_by.join(', '));
}

@ -39,7 +39,7 @@ const AmRoutes: FC = () => {
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
const config = result?.alertmanager_config;
const routes = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
const [routes, id2ExistingRoute] = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
const receivers = stringsToSelectableValues(
(config?.receivers ?? []).map((receiver: Receiver) => receiver.name)
@ -59,10 +59,13 @@ const AmRoutes: FC = () => {
);
const handleSave = (data: Partial<FormAmRoute>) => {
const newData = formAmRouteToAmRoute({
...routes,
...data,
});
const newData = formAmRouteToAmRoute(
{
...routes,
...data,
},
id2ExistingRoute
);
if (isRootRouteEditMode) {
exitRootRouteEditMode();

@ -6,7 +6,7 @@ import {
AlertmanagerGroup,
Silence,
SilenceCreatePayload,
SilenceMatcher,
Matcher,
} from 'app/plugins/datasource/alertmanager/types';
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
@ -91,7 +91,7 @@ export async function expireSilence(alertmanagerSourceName: string, silenceID: s
export async function fetchAlerts(
alertmanagerSourceName: string,
matchers?: SilenceMatcher[],
matchers?: Matcher[],
silenced = true,
active = true,
inhibited = true

@ -6,13 +6,14 @@ import { css } from '@emotion/css';
interface Props {
labelKey: string;
value: string;
isRegex?: boolean;
operator?: string;
onRemoveLabel?: () => void;
}
export const AlertLabel: FC<Props> = ({ labelKey, value, isRegex = false, onRemoveLabel }) => (
export const AlertLabel: FC<Props> = ({ labelKey, value, operator = '=', onRemoveLabel }) => (
<div className={useStyles(getStyles)}>
{labelKey}={isRegex && '~'}
{labelKey}
{operator}
{value}
{!!onRemoveLabel && <IconButton name="times" size="xs" onClick={onRemoveLabel} />}
</div>

@ -8,6 +8,7 @@ import {
FieldArray,
Form,
HorizontalGroup,
IconButton,
Input,
InputControl,
MultiSelect,
@ -47,9 +48,11 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<Form defaultValues={routes} onSubmit={onSave}>
{({ control, register, errors, setValue }) => (
<>
{/* @ts-ignore-check: react-hook-form made me do this */}
<input type="hidden" {...register('id')} />
{/* @ts-ignore-check: react-hook-form made me do this */}
<FieldArray name="matchers" control={control}>
{({ fields, append }) => (
{({ fields, append, remove }) => (
<>
<div>Matchers</div>
<div className={styles.matchersContainer}>
@ -57,18 +60,17 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
const localPath = `matchers[${index}]`;
return (
<HorizontalGroup key={field.id}>
<HorizontalGroup key={field.id} align="flex-start">
<Field
label="Label"
invalid={!!errors.matchers?.[index]?.label}
error={errors.matchers?.[index]?.label?.message}
label="Name"
invalid={!!errors.matchers?.[index]?.name}
error={errors.matchers?.[index]?.name?.message}
>
<Input
{...register(`${localPath}.label`, { required: 'Field is required' })}
defaultValue={field.label}
{...register(`${localPath}.name`, { required: 'Field is required' })}
defaultValue={field.name}
/>
</Field>
<span>=</span>
<Field
label="Value"
invalid={!!errors.matchers?.[index]?.value}
@ -82,6 +84,17 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<Field className={styles.matcherRegexField} label="Regex">
<Checkbox {...register(`${localPath}.isRegex`)} defaultChecked={field.isRegex} />
</Field>
<Field className={styles.matcherRegexField} label="Equal">
<Checkbox {...register(`${localPath}.isEqual`)} defaultChecked={field.isEqual} />
</Field>
<IconButton
className={styles.removeButton}
tooltip="Remove matcher"
name={'trash-alt'}
onClick={() => remove(index)}
>
Remove
</IconButton>
</HorizontalGroup>
);
})}
@ -286,6 +299,10 @@ const getStyles = (theme: GrafanaTheme2) => {
nestedPolicies: css`
margin-top: ${commonSpacing};
`,
removeButton: css`
margin-left: ${theme.spacing(1)};
margin-top: ${theme.spacing(2.5)};
`,
buttonGroup: css`
margin: ${theme.spacing(6)} 0 ${commonSpacing};

@ -8,10 +8,10 @@ import {
prepareItems,
removeCustomExpandedContent,
} from '../../utils/dynamicTable';
import { AlertLabels } from '../AlertLabels';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { AmRoutesExpandedForm } from './AmRoutesExpandedForm';
import { AmRoutesExpandedRead } from './AmRoutesExpandedRead';
import { Matchers } from '../silences/Matchers';
export interface AmRoutesTableProps {
isAddMode: boolean;
@ -67,17 +67,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({ isAddMode, onCancelAdd,
id: 'matchingCriteria',
label: 'Matching criteria',
// eslint-disable-next-line react/display-name
renderCell: (item) => (
<AlertLabels
labels={item.data.matchers.reduce(
(acc, matcher) => ({
...acc,
[matcher.label]: matcher.value,
}),
{}
)}
/>
),
renderCell: (item) => <Matchers matchers={item.data.matchers} />,
size: 10,
},
{

@ -9,7 +9,7 @@ import { ReceiversSection } from './ReceiversSection';
import { makeAMLink } from '../../utils/misc';
import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { isReceiverUsed } from '../../utils/alertmanager-config';
import { isReceiverUsed } from '../../utils/alertmanager';
import { useDispatch } from 'react-redux';
import { deleteReceiverAction } from '../../state/actions';

@ -2,10 +2,11 @@ import React, { useCallback } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '@grafana/ui';
import { css } from '@emotion/css';
import { SilenceMatcher } from 'app/plugins/datasource/alertmanager/types';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { AlertLabel } from '../AlertLabel';
import { matcherToOperator } from '../../utils/alertmanager';
type MatchersProps = { matchers: SilenceMatcher[]; onRemoveLabel?(index: number): void };
type MatchersProps = { matchers: Matcher[]; onRemoveLabel?(index: number): void };
export const Matchers = ({ matchers, onRemoveLabel }: MatchersProps) => {
const styles = useStyles(getStyles);
@ -21,13 +22,14 @@ export const Matchers = ({ matchers, onRemoveLabel }: MatchersProps) => {
return (
<div className={styles.wrapper}>
{matchers.map(({ name, value, isRegex }: SilenceMatcher, index) => {
{matchers.map((matcher, index) => {
const { name, value } = matcher;
return (
<AlertLabel
key={`${name}-${value}-${index}`}
labelKey={name}
value={value}
isRegex={isRegex}
operator={matcherToOperator(matcher)}
onRemoveLabel={!!onRemoveLabel ? () => removeLabel(index) : undefined}
/>
);

@ -1,16 +1,17 @@
import React, { FC } from 'react';
import { Button, Field, Input, InlineLabel, useStyles, Checkbox, IconButton } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { Button, Field, Input, Checkbox, IconButton, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { useFormContext, useFieldArray } from 'react-hook-form';
import { SilenceFormFields } from '../../types/silence-form';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
interface Props {
className?: string;
}
const MatchersField: FC<Props> = ({ className }) => {
const styles = useStyles(getStyles);
const styles = useStyles2(getStyles);
const formApi = useFormContext<SilenceFormFields>();
const {
register,
@ -24,6 +25,7 @@ const MatchersField: FC<Props> = ({ className }) => {
<div>
<div className={styles.matchers}>
{matchers.map((matcher, index) => {
console.log(matcher);
return (
<div className={styles.row} key={`${matcher.id}`}>
<Field
@ -39,7 +41,6 @@ const MatchersField: FC<Props> = ({ className }) => {
placeholder="name"
/>
</Field>
<InlineLabel className={styles.equalSign}>=</InlineLabel>
<Field
label="Value"
invalid={!!errors?.matchers?.[index]?.value}
@ -53,9 +54,12 @@ const MatchersField: FC<Props> = ({ className }) => {
placeholder="value"
/>
</Field>
<Field className={styles.regexCheckbox} label="Regex">
<Field label="Regex">
<Checkbox {...register(`matchers.${index}.isRegex` as const)} defaultChecked={matcher.isRegex} />
</Field>
<Field label="Equal">
<Checkbox {...register(`matchers.${index}.isEqual` as const)} defaultChecked={matcher.isEqual} />
</Field>
{matchers.length > 1 && (
<IconButton
className={styles.removeButton}
@ -75,7 +79,8 @@ const MatchersField: FC<Props> = ({ className }) => {
icon="plus"
variant="secondary"
onClick={() => {
append({ name: '', value: '', isRegex: false });
const newMatcher: Matcher = { name: '', value: '', isRegex: false, isEqual: true };
append(newMatcher);
}}
>
Add matcher
@ -86,34 +91,29 @@ const MatchersField: FC<Props> = ({ className }) => {
);
};
const getStyles = (theme: GrafanaTheme) => {
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
margin-top: ${theme.spacing.md};
margin-top: ${theme.spacing(2)};
`,
row: css`
display: flex;
align-items: flex-start;
flex-direction: row;
align-items: center;
background-color: ${theme.colors.bg2};
padding: ${theme.spacing.sm} ${theme.spacing.sm} 0 ${theme.spacing.sm};
`,
equalSign: css`
width: 28px;
justify-content: center;
margin-left: ${theme.spacing.xs};
margin-bottom: 0;
`,
regexCheckbox: css`
margin-left: ${theme.spacing.md};
background-color: ${theme.colors.background.secondary};
padding: ${theme.spacing(1)} ${theme.spacing(1)} 0 ${theme.spacing(1)};
& > * + * {
margin-left: ${theme.spacing(2)};
}
`,
removeButton: css`
margin-left: ${theme.spacing.sm};
margin-left: ${theme.spacing(1)};
margin-top: ${theme.spacing(2.5)};
`,
matchers: css`
max-width: 585px;
margin: ${theme.spacing.sm} 0;
padding-top: ${theme.spacing.xs};
margin: ${theme.spacing(1)} 0;
padding-top: ${theme.spacing(0.5)};
`,
};
};

@ -62,7 +62,7 @@ const getDefaultFormValues = (silence?: Silence): SilenceFormFields => {
createdBy: config.bootData.user.name,
duration: '2h',
isRegex: false,
matchers: [{ name: '', value: '', isRegex: false }],
matchers: [{ name: '', value: '', isRegex: false, isEqual: true }],
matcherName: '',
matcherValue: '',
timeZone: DefaultTimeZone,

@ -45,7 +45,7 @@ import {
ruleWithLocationToRuleIdentifier,
stringifyRuleIdentifier,
} from '../utils/rules';
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager-config';
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager';
import { backendSrv } from 'app/core/services/backend_srv';
export const fetchPromRulesAction = createAsyncThunk(

@ -1,11 +1,8 @@
export interface ArrayFieldMatcher {
label: string;
value: string;
isRegex: boolean;
}
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
export interface FormAmRoute {
matchers: ArrayFieldMatcher[];
id: string;
matchers: Matcher[];
continue: boolean;
receiver: string;
groupBy: string[];

@ -1,4 +1,4 @@
import { SilenceMatcher } from 'app/plugins/datasource/alertmanager/types';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { TimeZone } from '@grafana/data';
export type SilenceFormFields = {
@ -8,7 +8,7 @@ export type SilenceFormFields = {
timeZone: TimeZone;
duration: string;
comment: string;
matchers: SilenceMatcher[];
matchers: Matcher[];
createdBy: string;
matcherName: string;
matcherValue: string;

@ -1,30 +0,0 @@
import { AlertManagerCortexConfig, Route } from 'app/plugins/datasource/alertmanager/types';
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
// add default receiver if it does not exist
if (!config.alertmanager_config.receivers) {
config.alertmanager_config.receivers = [{ name: 'default ' }];
}
// add default route if it does not exists
if (!config.alertmanager_config.route) {
config.alertmanager_config.route = {
receiver: config.alertmanager_config.receivers![0].name,
};
}
if (!config.template_files) {
config.template_files = {};
}
return config;
}
function isReceiverUsedInRoute(receiver: string, route: Route): boolean {
return (
(route.receiver === receiver || route.routes?.some((route) => isReceiverUsedInRoute(receiver, route))) ?? false
);
}
export function isReceiverUsed(receiver: string, config: AlertManagerCortexConfig): boolean {
return (
(config.alertmanager_config.route && isReceiverUsedInRoute(receiver, config.alertmanager_config.route)) ?? false
);
}

@ -0,0 +1,69 @@
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { parseMatcher, stringifyMatcher } from './alertmanager';
describe('Alertmanager utils', () => {
describe('parseMatcher', () => {
it('should parse operators correctly', () => {
expect(parseMatcher('foo=bar')).toEqual<Matcher>({
name: 'foo',
value: 'bar',
isRegex: false,
isEqual: true,
});
expect(parseMatcher('foo!=bar')).toEqual<Matcher>({
name: 'foo',
value: 'bar',
isRegex: false,
isEqual: false,
});
expect(parseMatcher('foo =~bar')).toEqual<Matcher>({
name: 'foo',
value: 'bar',
isRegex: true,
isEqual: true,
});
expect(parseMatcher('foo!~ bar')).toEqual<Matcher>({
name: 'foo',
value: 'bar',
isRegex: true,
isEqual: false,
});
});
it('should parse escaped values correctly', () => {
expect(parseMatcher('foo=~"bar\\"baz\\""')).toEqual<Matcher>({
name: 'foo',
value: 'bar"baz"',
isRegex: true,
isEqual: true,
});
expect(parseMatcher('foo=~bar\\"baz\\"')).toEqual<Matcher>({
name: 'foo',
value: 'bar"baz"',
isRegex: true,
isEqual: true,
});
});
it('should parse multiple operators values correctly', () => {
expect(parseMatcher('foo=~bar=baz!=bad!~br')).toEqual<Matcher>({
name: 'foo',
value: 'bar=baz!=bad!~br',
isRegex: true,
isEqual: true,
});
});
});
describe('stringifyMatcher', () => {
it('should stringify matcher correctly', () => {
expect(
stringifyMatcher({
name: 'foo',
value: 'boo="bar"',
isRegex: true,
isEqual: false,
})
).toEqual('foo!~"boo=\\"bar\\""');
});
});
});

@ -0,0 +1,95 @@
import { AlertManagerCortexConfig, MatcherOperator, Route, Matcher } from 'app/plugins/datasource/alertmanager/types';
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
// add default receiver if it does not exist
if (!config.alertmanager_config.receivers) {
config.alertmanager_config.receivers = [{ name: 'default ' }];
}
// add default route if it does not exists
if (!config.alertmanager_config.route) {
config.alertmanager_config.route = {
receiver: config.alertmanager_config.receivers![0].name,
};
}
if (!config.template_files) {
config.template_files = {};
}
return config;
}
function isReceiverUsedInRoute(receiver: string, route: Route): boolean {
return (
(route.receiver === receiver || route.routes?.some((route) => isReceiverUsedInRoute(receiver, route))) ?? false
);
}
export function isReceiverUsed(receiver: string, config: AlertManagerCortexConfig): boolean {
return (
(config.alertmanager_config.route && isReceiverUsedInRoute(receiver, config.alertmanager_config.route)) ?? false
);
}
export function matcherToOperator(matcher: Matcher): MatcherOperator {
if (matcher.isEqual) {
if (matcher.isRegex) {
return MatcherOperator.regex;
} else {
return MatcherOperator.equal;
}
} else if (matcher.isRegex) {
return MatcherOperator.notRegex;
} else {
return MatcherOperator.notEqual;
}
}
const matcherOperators = [
MatcherOperator.regex,
MatcherOperator.notRegex,
MatcherOperator.notEqual,
MatcherOperator.equal,
];
function unescapeMatcherValue(value: string) {
let trimmed = value.trim().replace(/\\"/g, '"');
if (trimmed.startsWith('"') && trimmed.endsWith('"') && !trimmed.endsWith('\\"')) {
trimmed = trimmed.substr(1, trimmed.length - 2);
}
return trimmed.replace(/\\"/g, '"');
}
function escapeMatcherValue(value: string) {
return '"' + value.replace(/"/g, '\\"') + '"';
}
export function stringifyMatcher(matcher: Matcher): string {
return `${matcher.name}${matcherToOperator(matcher)}${escapeMatcherValue(matcher.value)}`;
}
export function parseMatcher(matcher: string): Matcher {
const trimmed = matcher.trim();
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
throw new Error(`PromQL matchers not supported yet, sorry! PromQL matcher found: ${trimmed}`);
}
const operatorsFound = matcherOperators
.map((op): [MatcherOperator, number] => [op, trimmed.indexOf(op)])
.filter(([_, idx]) => idx > -1)
.sort((a, b) => a[1] - b[1]);
if (!operatorsFound.length) {
throw new Error(`Invalid matcher: ${trimmed}`);
}
const [operator, idx] = operatorsFound[0];
const name = trimmed.substr(0, idx).trim();
const value = unescapeMatcherValue(trimmed.substr(idx + operator.length).trim());
if (!name) {
throw new Error(`Invalid matcher: ${trimmed}`);
}
return {
name,
value,
isRegex: operator === MatcherOperator.regex || operator === MatcherOperator.notRegex,
isEqual: operator === MatcherOperator.equal || operator === MatcherOperator.regex,
};
}

@ -1,22 +1,21 @@
import { SelectableValue } from '@grafana/data';
import { Validate } from 'react-hook-form';
import { Route } from 'app/plugins/datasource/alertmanager/types';
import { FormAmRoute, ArrayFieldMatcher } from '../types/amroutes';
import { Matcher, Route } from 'app/plugins/datasource/alertmanager/types';
import { FormAmRoute } from '../types/amroutes';
import { parseInterval, timeOptions } from './time';
import { parseMatcher, stringifyMatcher } from './alertmanager';
const defaultValueAndType: [string, string] = ['', timeOptions[0].value];
const matchersToArrayFieldMatchers = (
matchers: Record<string, string> | undefined,
isRegex: boolean
): ArrayFieldMatcher[] =>
const matchersToArrayFieldMatchers = (matchers: Record<string, string> | undefined, isRegex: boolean): Matcher[] =>
Object.entries(matchers ?? {}).reduce(
(acc, [label, value]) => [
(acc, [name, value]) => [
...acc,
{
label,
name,
value,
isRegex: isRegex,
isEqual: true,
},
],
[]
@ -43,13 +42,15 @@ const selectableValueToString = (selectableValue: SelectableValue<string>): stri
const selectableValuesToStrings = (arr: Array<SelectableValue<string>> | undefined): string[] =>
(arr ?? []).map(selectableValueToString);
export const emptyArrayFieldMatcher: ArrayFieldMatcher = {
label: '',
export const emptyArrayFieldMatcher: Matcher = {
name: '',
value: '',
isRegex: false,
isEqual: true,
};
export const emptyRoute: FormAmRoute = {
id: '',
matchers: [emptyArrayFieldMatcher],
groupBy: [],
routes: [],
@ -63,50 +64,59 @@ export const emptyRoute: FormAmRoute = {
repeatIntervalValueType: timeOptions[0].value,
};
export const amRouteToFormAmRoute = (route: Route | undefined): FormAmRoute => {
//returns route, and a record mapping id to existing route route
export const amRouteToFormAmRoute = (route: Route | undefined): [FormAmRoute, Record<string, Route>] => {
if (!route || Object.keys(route).length === 0) {
return emptyRoute;
return [emptyRoute, {}];
}
const [groupWaitValue, groupWaitValueType] = intervalToValueAndType(route.group_wait);
const [groupIntervalValue, groupIntervalValueType] = intervalToValueAndType(route.group_interval);
const [repeatIntervalValue, repeatIntervalValueType] = intervalToValueAndType(route.repeat_interval);
return {
matchers: [
...matchersToArrayFieldMatchers(route.match, false),
...matchersToArrayFieldMatchers(route.match_re, true),
],
continue: route.continue ?? false,
receiver: route.receiver ?? '',
groupBy: route.group_by ?? [],
groupWaitValue,
groupWaitValueType,
groupIntervalValue,
groupIntervalValueType,
repeatIntervalValue,
repeatIntervalValueType,
routes: (route.routes ?? []).map(amRouteToFormAmRoute),
const id = String(Math.random());
const id2route = {
[id]: route,
};
const formRoutes: FormAmRoute[] = [];
route.routes?.forEach((subRoute) => {
const [subFormRoute, subId2Route] = amRouteToFormAmRoute(subRoute);
formRoutes.push(subFormRoute);
Object.assign(id2route, subId2Route);
});
return [
{
id,
matchers: [
...(route.matchers?.map(parseMatcher) ?? []),
...matchersToArrayFieldMatchers(route.match, false),
...matchersToArrayFieldMatchers(route.match_re, true),
],
continue: route.continue ?? false,
receiver: route.receiver ?? '',
groupBy: route.group_by ?? [],
groupWaitValue,
groupWaitValueType,
groupIntervalValue,
groupIntervalValueType,
repeatIntervalValue,
repeatIntervalValueType,
routes: formRoutes,
},
id2route,
];
};
export const formAmRouteToAmRoute = (formAmRoute: FormAmRoute): Route => {
export const formAmRouteToAmRoute = (formAmRoute: FormAmRoute, id2ExistingRoute: Record<string, Route>): Route => {
const existing: Route | undefined = id2ExistingRoute[formAmRoute.id];
const amRoute: Route = {
...(existing ?? {}),
continue: formAmRoute.continue,
group_by: formAmRoute.groupBy,
...Object.values(formAmRoute.matchers).reduce(
(acc, { label, value, isRegex }) => {
const target = acc[isRegex ? 'match_re' : 'match'];
target![label] = value;
return acc;
},
{
match: {},
match_re: {},
} as Pick<Route, 'match' | 'match_re'>
),
matchers: formAmRoute.matchers.length ? formAmRoute.matchers.map(stringifyMatcher) : undefined,
match: undefined,
match_re: undefined,
group_wait: formAmRoute.groupWaitValue
? `${formAmRoute.groupWaitValue}${formAmRoute.groupWaitValueType}`
: undefined,
@ -116,7 +126,7 @@ export const formAmRouteToAmRoute = (formAmRoute: FormAmRoute): Route => {
repeat_interval: formAmRoute.repeatIntervalValue
? `${formAmRoute.repeatIntervalValue}${formAmRoute.repeatIntervalValueType}`
: undefined,
routes: formAmRoute.routes.map(formAmRouteToAmRoute),
routes: formAmRoute.routes.map((subRoute) => formAmRouteToAmRoute(subRoute, id2ExistingRoute)),
};
if (formAmRoute.receiver) {

@ -98,6 +98,7 @@ export type Route = {
receiver?: string;
group_by?: string[];
continue?: boolean;
matchers?: string[];
match?: Record<string, string>;
match_re?: Record<string, string>;
group_wait?: string;
@ -142,10 +143,11 @@ export type AlertmanagerConfig = {
receivers?: Receiver[];
};
export type SilenceMatcher = {
export type Matcher = {
name: string;
value: string;
isRegex: boolean;
isEqual: boolean;
};
export enum SilenceState {
@ -160,9 +162,16 @@ export enum AlertState {
Suppressed = 'suppressed',
}
export enum MatcherOperator {
equal = '=',
notEqual = '!=',
regex = '=~',
notRegex = '!~',
}
export type Silence = {
id: string;
matchers?: SilenceMatcher[];
matchers?: Matcher[];
startsAt: string;
endsAt: string;
updatedAt: string;
@ -175,7 +184,7 @@ export type Silence = {
export type SilenceCreatePayload = {
id?: string;
matchers?: SilenceMatcher[];
matchers?: Matcher[];
startsAt: string;
endsAt: string;
createdBy: string;

Loading…
Cancel
Save