grafana_bak/public/app/features/alerting/unified/utils/notification-policies.ts
2025-04-01 10:38:02 +09:00

233 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { isArray, pick, reduce } from 'lodash';
import { AlertmanagerGroup, ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { Labels } from 'app/types/unified-alerting-dto';
import { Label, isLabelMatch, matchLabelsSet, normalizeMatchers, unquoteWithUnescape } from './matchers';
// If a policy has no matchers it still can be a match, hence matchers can be empty and match can be true
// So we cannot use null as an indicator of no match
interface LabelMatchResult {
match: boolean;
matcher: ObjectMatcher | null;
}
export const INHERITABLE_KEYS = ['receiver', 'group_by', 'group_wait', 'group_interval', 'repeat_interval'] as const;
export type InheritableKeys = typeof INHERITABLE_KEYS;
export type InheritableProperties = Pick<Route, InheritableKeys[number]>;
type LabelsMatch = Map<Label, LabelMatchResult>;
interface MatchingResult {
matches: boolean;
labelsMatch: LabelsMatch;
}
// returns a match results for given set of matchers (from a policy for instance) and a set of labels
export function matchLabels(matchers: ObjectMatcher[], labels: Label[]): MatchingResult {
const matches = matchLabelsSet(matchers, labels);
// create initial map of label => match result
const labelsMatch: LabelsMatch = new Map(labels.map((label) => [label, { match: false, matcher: null }]));
// for each matcher, check which label it matched for
matchers.forEach((matcher) => {
const matchingLabel = labels.find((label) => isLabelMatch(matcher, label));
// record that matcher for the label
if (matchingLabel) {
labelsMatch.set(matchingLabel, {
match: true,
matcher,
});
}
});
return { matches, labelsMatch };
}
export interface AlertInstanceMatch {
instance: Labels;
labelsMatch: LabelsMatch;
}
export interface RouteMatchResult<T extends Route> {
route: T;
labelsMatch: LabelsMatch;
}
// Match does a depth-first left-to-right search through the route tree
// and returns the matching routing nodes.
// If the current node is not a match, return nothing
// Normalization should have happened earlier in the code
function findMatchingRoutes<T extends Route>(route: T, labels: Label[]): Array<RouteMatchResult<T>> {
let childMatches: Array<RouteMatchResult<T>> = [];
// If the current node is not a match, return nothing
const matchResult = matchLabels(route.object_matchers ?? [], labels);
if (!matchResult.matches) {
return [];
}
// If the current node matches, recurse through child nodes
if (route.routes) {
for (const child of route.routes) {
const matchingChildren = findMatchingRoutes(child, labels);
// TODO how do I solve this typescript thingy? It looks correct to me /shrug
// @ts-ignore
childMatches = childMatches.concat(matchingChildren);
// we have matching children and we don't want to continue, so break here
if (matchingChildren.length && !child.continue) {
break;
}
}
}
// If no child nodes were matches, the current node itself is a match.
if (childMatches.length === 0) {
childMatches.push({ route, labelsMatch: matchResult.labelsMatch });
}
return childMatches;
}
// This is a performance improvement to normalize matchers only once and use the normalized version later on
export function normalizeRoute(rootRoute: RouteWithID): RouteWithID {
function normalizeRoute(route: RouteWithID) {
route.object_matchers = normalizeMatchers(route);
delete route.matchers;
delete route.match;
delete route.match_re;
route.routes?.forEach(normalizeRoute);
}
const normalizedRootRoute = structuredClone(rootRoute);
normalizeRoute(normalizedRootRoute);
return normalizedRootRoute;
}
export function unquoteRouteMatchers(route: RouteWithID): RouteWithID {
function unquoteRoute(route: RouteWithID) {
route.object_matchers = route.object_matchers?.map(([name, operator, value]) => {
return [unquoteWithUnescape(name), operator, unquoteWithUnescape(value)];
});
route.routes?.forEach(unquoteRoute);
}
const unwrappedRootRoute = structuredClone(route);
unquoteRoute(unwrappedRootRoute);
return unwrappedRootRoute;
}
/**
* find all of the groups that have instances that match the route, thay way we can find all instances
* (and their grouping) for the given route
*/
function findMatchingAlertGroups(
routeTree: Route,
route: Route,
alertGroups: AlertmanagerGroup[]
): AlertmanagerGroup[] {
const matchingGroups: AlertmanagerGroup[] = [];
return alertGroups.reduce((acc, group) => {
// find matching alerts in the current group
const matchingAlerts = group.alerts.filter((alert) => {
const labels = Object.entries(alert.labels);
return findMatchingRoutes(routeTree, labels).some((matchingRoute) => matchingRoute.route === route);
});
// if the groups has any alerts left after matching, add it to the results
if (matchingAlerts.length) {
acc.push({
...group,
alerts: matchingAlerts,
});
}
return acc;
}, matchingGroups);
}
// inherited properties are config properties that exist on the parent route (or its inherited properties) but not on the child route
function getInheritedProperties(
parentRoute: Route,
childRoute: Route,
propertiesParentInherited?: InheritableProperties
): InheritableProperties {
const propsFromParent: InheritableProperties = pick(parentRoute, INHERITABLE_KEYS);
const inheritableProperties: InheritableProperties = {
...propsFromParent,
...propertiesParentInherited,
};
const inherited = reduce(
inheritableProperties,
(inheritedProperties: InheritableProperties, parentValue, property) => {
const parentHasValue = parentValue != null;
const inheritableValues = [undefined, '', null];
// @ts-ignore
const childIsInheriting = inheritableValues.some((value) => childRoute[property] === value);
const inheritFromValue = childIsInheriting && parentHasValue;
const inheritEmptyGroupByFromParent =
property === 'group_by' &&
parentHasValue &&
isArray(childRoute[property]) &&
childRoute[property]?.length === 0;
const inheritFromParent = inheritFromValue || inheritEmptyGroupByFromParent;
if (inheritFromParent) {
// @ts-ignore
inheritedProperties[property] = parentValue;
}
return inheritedProperties;
},
{}
);
return inherited;
}
/**
* This function will compute the full tree with inherited properties this is mostly used for search and filtering
*/
export function computeInheritedTree<T extends Route>(parent: T): T {
return {
...parent,
routes: parent.routes?.map((child) => {
const inheritedProperties = getInheritedProperties(parent, child);
return computeInheritedTree({
...child,
...inheritedProperties,
});
}),
};
}
// recursive function to rename receivers in all routes (notification policies)
function renameReceiverInRoute(route: Route, oldName: string, newName: string) {
const updated: Route = {
...route,
};
if (updated.receiver === oldName) {
updated.receiver = newName;
}
if (updated.routes) {
updated.routes = updated.routes.map((route) => renameReceiverInRoute(route, oldName, newName));
}
return updated;
}
export { findMatchingAlertGroups, findMatchingRoutes, getInheritedProperties, renameReceiverInRoute };