grafana/public/app/features/alerting/unified/utils/amroutes.ts

274 lines
8.9 KiB
TypeScript

import { uniqueId } from 'lodash';
import { SelectableValue } from '@grafana/data';
import { MatcherOperator, ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { FormAmRoute } from '../types/amroutes';
import { MatcherFieldValue } from '../types/silence-form';
import { matcherToMatcherField } from './alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
import { normalizeMatchers, parseMatcher, quoteWithEscape, unquoteWithUnescape } from './matchers';
import { findExistingRoute } from './routeTree';
import { isValidPrometheusDuration, safeParseDurationstr } from './time';
const matchersToArrayFieldMatchers = (
matchers: Record<string, string> | undefined,
isRegex: boolean
): MatcherFieldValue[] =>
Object.entries(matchers ?? {}).reduce<MatcherFieldValue[]>(
(acc, [name, value]) => [
...acc,
{
name,
value,
operator: isRegex ? MatcherOperator.regex : MatcherOperator.equal,
},
],
[]
);
const selectableValueToString = (selectableValue: SelectableValue<string>): string => selectableValue.value!;
const selectableValuesToStrings = (arr: Array<SelectableValue<string>> | undefined): string[] =>
(arr ?? []).map(selectableValueToString);
export const emptyArrayFieldMatcher: MatcherFieldValue = {
name: '',
value: '',
operator: MatcherOperator.equal,
};
// Default route group_by labels for newly created routes.
export const defaultGroupBy = ['grafana_folder', 'alertname'];
// Common route group_by options for multiselect drop-down
export const commonGroupByOptions = [
{ label: 'grafana_folder', value: 'grafana_folder', isFixed: true },
{ label: 'alertname', value: 'alertname', isFixed: true },
{ label: 'Disable (...)', value: '...' },
];
export const emptyRoute: FormAmRoute = {
id: '',
overrideGrouping: false,
groupBy: defaultGroupBy,
object_matchers: [],
routes: [],
continue: false,
receiver: '',
overrideTimings: false,
groupWaitValue: '',
groupIntervalValue: '',
repeatIntervalValue: '',
muteTimeIntervals: [],
};
// add unique identifiers to each route in the route tree, that way we can figure out what route we've edited / deleted
export function addUniqueIdentifierToRoute(route: Route): RouteWithID {
return {
id: uniqueId('route-'),
...route,
routes: (route.routes ?? []).map(addUniqueIdentifierToRoute),
};
}
//returns route, and a record mapping id to existing route
export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): FormAmRoute => {
if (!route) {
return emptyRoute;
}
const id = 'id' in route ? route.id : uniqueId('route-');
if (Object.keys(route).length === 0) {
const formAmRoute = { ...emptyRoute, id };
return formAmRoute;
}
const formRoutes: FormAmRoute[] = [];
route.routes?.forEach((subRoute) => {
const subFormRoute = amRouteToFormAmRoute(subRoute);
formRoutes.push(subFormRoute);
});
const objectMatchers =
route.object_matchers?.map((matcher) => ({ name: matcher[0], operator: matcher[1], value: matcher[2] })) ?? [];
const matchers =
route.matchers
?.map((matcher) => matcherToMatcherField(parseMatcher(matcher)))
.map(({ name, operator, value }) => ({
name,
operator,
value: unquoteWithUnescape(value),
})) ?? [];
return {
id,
// Frontend migration to use object_matchers instead of matchers, match, and match_re
object_matchers: [
...matchers,
...objectMatchers,
...matchersToArrayFieldMatchers(route.match, false),
...matchersToArrayFieldMatchers(route.match_re, true),
],
continue: route.continue ?? false,
receiver: route.receiver ?? '',
overrideGrouping: Array.isArray(route.group_by) && route.group_by.length > 0,
groupBy: route.group_by ?? undefined,
overrideTimings: [route.group_wait, route.group_interval, route.repeat_interval].some(Boolean),
groupWaitValue: route.group_wait ?? '',
groupIntervalValue: route.group_interval ?? '',
repeatIntervalValue: route.repeat_interval ?? '',
routes: formRoutes,
muteTimeIntervals: route.mute_time_intervals ?? [],
};
};
// convert a FormAmRoute to a Route
export const formAmRouteToAmRoute = (
alertManagerSourceName: string,
formAmRoute: Partial<FormAmRoute>,
routeTree: RouteWithID
): Route => {
const existing = findExistingRoute(formAmRoute.id ?? '', routeTree);
const {
overrideGrouping,
groupBy,
overrideTimings,
groupWaitValue,
groupIntervalValue,
repeatIntervalValue,
receiver,
} = formAmRoute;
// "undefined" means "inherit from the parent policy", currently supported by group_by, group_wait, group_interval, and repeat_interval
const INHERIT_FROM_PARENT = undefined;
const group_by = overrideGrouping ? groupBy : INHERIT_FROM_PARENT;
const overrideGroupWait = overrideTimings && groupWaitValue;
const group_wait = overrideGroupWait ? groupWaitValue : INHERIT_FROM_PARENT;
const overrideGroupInterval = overrideTimings && groupIntervalValue;
const group_interval = overrideGroupInterval ? groupIntervalValue : INHERIT_FROM_PARENT;
const overrideRepeatInterval = overrideTimings && repeatIntervalValue;
const repeat_interval = overrideRepeatInterval ? repeatIntervalValue : INHERIT_FROM_PARENT;
// Empty matcher values are valid. Such matchers require specified label to not exists
const object_matchers: ObjectMatcher[] | undefined = formAmRoute.object_matchers
?.filter((route) => route.name && route.operator && route.value !== null && route.value !== undefined)
.map(({ name, operator, value }) => [name, operator, value]);
const routes = formAmRoute.routes?.map((subRoute) =>
formAmRouteToAmRoute(alertManagerSourceName, subRoute, routeTree)
);
const amRoute: Route = {
...(existing ?? {}),
continue: formAmRoute.continue,
group_by: group_by,
object_matchers: object_matchers,
match: undefined, // DEPRECATED: Use matchers
match_re: undefined, // DEPRECATED: Use matchers
group_wait,
group_interval,
repeat_interval,
routes: routes,
mute_time_intervals: formAmRoute.muteTimeIntervals,
receiver: receiver,
};
// non-Grafana managed rules should use "matchers", Grafana-managed rules should use "object_matchers"
// Grafana maintains a fork of AM to support all utf-8 characters in the "object_matchers" property values but this
// does not exist in upstream AlertManager
if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) {
amRoute.matchers = formAmRoute.object_matchers?.map(
({ name, operator, value }) => `${name}${operator}${quoteWithEscape(value)}`
);
amRoute.object_matchers = undefined;
} else {
amRoute.object_matchers = normalizeMatchers(amRoute);
amRoute.matchers = undefined;
}
if (formAmRoute.receiver) {
amRoute.receiver = formAmRoute.receiver;
}
return amRoute;
};
export const stringToSelectableValue = (str: string): SelectableValue<string> => ({
label: str,
value: str,
});
export const stringsToSelectableValues = (arr: string[] | undefined): Array<SelectableValue<string>> =>
(arr ?? []).map(stringToSelectableValue);
export const mapSelectValueToString = (selectableValue: SelectableValue<string>): string | null => {
// this allows us to deal with cleared values
if (selectableValue === null) {
return null;
}
if (!selectableValue) {
return '';
}
return selectableValueToString(selectableValue) ?? '';
};
export const mapMultiSelectValueToStrings = (
selectableValues: Array<SelectableValue<string>> | undefined
): string[] => {
if (!selectableValues) {
return [];
}
return selectableValuesToStrings(selectableValues);
};
export function promDurationValidator(duration?: string) {
if (!duration || duration.length === 0) {
return true;
}
return isValidPrometheusDuration(duration) || 'Invalid duration format. Must be {number}{time_unit}';
}
// function to convert ObjectMatchers to a array of strings
export const objectMatchersToString = (matchers: ObjectMatcher[]): string[] => {
return matchers.map((matcher) => {
const [name, operator, value] = matcher;
return `${name}${operator}${value}`;
});
};
export const repeatIntervalValidator = (repeatInterval: string, groupInterval = '') => {
if (repeatInterval.length === 0) {
return true;
}
const validRepeatInterval = promDurationValidator(repeatInterval);
const validGroupInterval = promDurationValidator(groupInterval);
if (validRepeatInterval !== true) {
return validRepeatInterval;
}
if (validGroupInterval !== true) {
return validGroupInterval;
}
const repeatDuration = safeParseDurationstr(repeatInterval);
const groupDuration = safeParseDurationstr(groupInterval);
const isRepeatLowerThanGroupDuration = groupDuration !== 0 && repeatDuration < groupDuration;
return isRepeatLowerThanGroupDuration ? 'Repeat interval should be higher or equal to Group interval' : true;
};