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; type LabelsMatch = Map; 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 { 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(route: T, labels: Label[]): Array> { let childMatches: Array> = []; // 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(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 };