diff --git a/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx b/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx index cd320ad193e..ac99340ed9a 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx @@ -12,7 +12,7 @@ import { HoverCard } from '../HoverCard'; type MatchersProps = { matchers: ObjectMatcher[] }; // renders the first N number of matchers -const Matchers: FC = ({ matchers }) => { +const Matchers: FC = React.forwardRef(({ matchers }, ref) => { const styles = useStyles2(getStyles); const NUM_MATCHERS = 5; @@ -22,7 +22,7 @@ const Matchers: FC = ({ matchers }) => { const hasMoreMatchers = rest.length > 0; return ( - + {firstFew.map((matcher) => ( @@ -48,7 +48,9 @@ const Matchers: FC = ({ matchers }) => { ); -}; +}); + +Matchers.displayName = 'Matchers'; interface MatcherBadgeProps { matcher: ObjectMatcher; diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreviewByAlertManager.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreviewByAlertManager.tsx index c99c14239cd..aae1b53b845 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreviewByAlertManager.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreviewByAlertManager.tsx @@ -22,10 +22,8 @@ function NotificationPreviewByAlertManager({ }) { const styles = useStyles2(getStyles); - const { routesByIdMap, receiversByName, matchingMap, loading, error } = useAlertmanagerNotificationRoutingPreview( - alertManagerSource.name, - potentialInstances - ); + const { routesByIdMap, receiversByName, matchingMap, matchingMapPath, loading, error } = + useAlertmanagerNotificationRoutingPreview(alertManagerSource.name, potentialInstances); if (error) { return ( @@ -74,6 +72,8 @@ function NotificationPreviewByAlertManager({ key={routeId} routesByIdMap={routesByIdMap} alertManagerSourceName={alertManagerSource.name} + matchingMap={matchingMap} + matchingMapPath={matchingMapPath} /> ); })} diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx index a38a242e7b2..fa2c6d5a795 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx @@ -25,6 +25,8 @@ function NotificationRouteHeader({ alertManagerSourceName, expandRoute, onExpandRouteClick, + matchingMap, + matchingMapPath, }: { route: RouteWithPath; receiver: Receiver; @@ -33,6 +35,8 @@ function NotificationRouteHeader({ alertManagerSourceName: string; expandRoute: boolean; onExpandRouteClick: (expand: boolean) => void; + matchingMap: Map; + matchingMapPath: Map; }) { const styles = useStyles2(getStyles); const [showDetails, setShowDetails] = useState(false); @@ -82,6 +86,8 @@ function NotificationRouteHeader({ receiver={receiver} routesByIdMap={routesByIdMap} alertManagerSourceName={alertManagerSourceName} + matchingMap={matchingMap} + matchingMapPath={matchingMapPath} /> )} @@ -94,6 +100,8 @@ interface NotificationRouteProps { instanceMatches: AlertInstanceMatch[]; routesByIdMap: Map; alertManagerSourceName: string; + matchingMap: Map; + matchingMapPath: Map; } export function NotificationRoute({ @@ -102,26 +110,35 @@ export function NotificationRoute({ receiver, routesByIdMap, alertManagerSourceName, + matchingMap, + matchingMapPath, }: NotificationRouteProps) { const styles = useStyles2(getStyles); const [expandRoute, setExpandRoute] = useToggle(false); const GREY_COLOR_INDEX = 9; + const instanceMatchesUnique = [ + ...new Map( + instanceMatches.map((matchInstance) => [JSON.stringify(matchInstance.instance), matchInstance]) + ).values(), + ]; return (
{expandRoute && (
- {instanceMatches.map((instanceMatch) => { + {instanceMatchesUnique.map((instanceMatch) => { const matchArray = Array.from(instanceMatch.labelsMatch.entries()); let matchResult = matchArray.map(([label, matchResult]) => ({ label: `${label[0]}=${label[1]}`, diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx index ba8c8bb6426..72ca1291294 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx @@ -3,19 +3,56 @@ import { compact } from 'lodash'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, Icon, Modal, useStyles2 } from '@grafana/ui'; +import { Button, Icon, Modal, TagList, Tooltip, useStyles2 } from '@grafana/ui'; import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types'; import { Stack } from '../../../../../../plugins/datasource/parca/QueryEditor/Stack'; import { getNotificationsPermissions } from '../../../utils/access-control'; +import { Label } from '../../../utils/matchers'; import { makeAMLink } from '../../../utils/misc'; +import { AlertInstanceMatch, LabelMatchResult } from '../../../utils/notification-policies'; import { Authorize } from '../../Authorize'; import { Matchers } from '../../notification-policies/Matchers'; import { NotificationPolicyMatchers } from './NotificationPolicyMatchers'; import { hasEmptyMatchers, isDefaultPolicy, RouteWithPath } from './route'; -function PolicyPath({ route, routesByIdMap }: { routesByIdMap: Map; route: RouteWithPath }) { +export const LabelsMatching = ({ + routeId, + matchingMapPath, +}: { + routeId: string; + matchingMap: Map; + matchingMapPath: Map; +}) => { + const matching = matchingMapPath.get(routeId); + if (!matching) { + return null; + } + const matchingInstances: AlertInstanceMatch[] | undefined = matchingMapPath.get(routeId); + // get array of labelsMatch from mapchingInstances + const valuesIterator = matchingInstances?.map((instance) => instance.labelsMatch)?.values() ?? []; + const labelMaps: Array> = Array.from(valuesIterator); + const labels = labelMaps + .map((m) => Array.from(m.entries() ?? []).filter(([label, result]) => result.match)) + .flat() + .map(([label, _]) => label); + // get array of strings from labels, remove duplicated + const labelsStringArray = Array.from(new Set(labels.map((label: Label) => label[0] + '=' + label[1]))); + return ; +}; + +function PolicyPath({ + route, + routesByIdMap, + matchingMap, + matchingMapPath, +}: { + routesByIdMap: Map; + route: RouteWithPath; + matchingMapPath: Map; + matchingMap: Map; +}) { const styles = useStyles2(getStyles); const routePathIds = route.path?.slice(1) ?? []; const routePathObjects = [...compact(routePathIds.map((id) => routesByIdMap.get(id))), route]; @@ -30,7 +67,18 @@ function PolicyPath({ route, routesByIdMap }: { routesByIdMap: MapNo matchers
) : ( - + + } + > + + )}
@@ -46,6 +94,8 @@ interface NotificationRouteDetailsModalProps { receiver: Receiver; routesByIdMap: Map; alertManagerSourceName: string; + matchingMap: Map; + matchingMapPath: Map; } export function NotificationRouteDetailsModal({ @@ -54,6 +104,8 @@ export function NotificationRouteDetailsModal({ receiver, routesByIdMap, alertManagerSourceName, + matchingMap, + matchingMapPath, }: NotificationRouteDetailsModalProps) { const styles = useStyles2(getStyles); const isDefault = isDefaultPolicy(route); @@ -84,7 +136,12 @@ export function NotificationRouteDetailsModal({ {!isDefault && ( <>
Notification policy path
- + )}
diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts index 40c21e6a973..ee204a9b41a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts @@ -49,7 +49,10 @@ export const useAlertmanagerNotificationRoutingPreview = ( // match labels in the tree => map of notification policies and the alert instances (list of labels) in each one const { - value: matchingMap = new Map(), + value: matchingMap = { + result: new Map(), + resultPath: new Map(), + }, loading: matchingLoading, error: matchingError, } = useAsync(async () => { @@ -62,7 +65,8 @@ export const useAlertmanagerNotificationRoutingPreview = ( return { routesByIdMap, receiversByName, - matchingMap: matchingMap, + matchingMap: matchingMap.result, + matchingMapPath: matchingMap.resultPath, loading: configLoading || matchingLoading, error: configError ?? matchingError, }; diff --git a/public/app/features/alerting/unified/routeGroupsMatcher.ts b/public/app/features/alerting/unified/routeGroupsMatcher.ts index 9b5dd3c8c6f..84b10a9c22e 100644 --- a/public/app/features/alerting/unified/routeGroupsMatcher.ts +++ b/public/app/features/alerting/unified/routeGroupsMatcher.ts @@ -25,15 +25,23 @@ export const routeGroupsMatcher = { return routeGroupsMap; }, - matchInstancesToRoute(routeTree: RouteWithID, instancesToMatch: Labels[]): Map { + matchInstancesToRoute( + routeTree: RouteWithID, + instancesToMatch: Labels[] + ): { result: Map; resultPath: Map } { const result = new Map(); + const resultPath = new Map(); const normalizedRootRoute = normalizeRoute(routeTree); + // find matching routes for each instance and add them to the results map and the path map instancesToMatch.forEach((instance) => { - const matchingRoutes = findMatchingRoutes(normalizedRootRoute, Object.entries(instance)); + const { matchesResult: matchingRoutes, matchesPath } = findMatchingRoutes( + normalizedRootRoute, + Object.entries(instance) + ); + // Only to convert Label[] to Labels[] - needs better approach matchingRoutes.forEach(({ route, details, labelsMatch }) => { - // Only to convert Label[] to Labels[] - needs better approach const matchDetails = new Map( Array.from(details.entries()).map(([matcher, labels]) => [matcher, Object.fromEntries(labels)]) ); @@ -45,9 +53,21 @@ export const routeGroupsMatcher = { result.set(route.id, [{ instance, matchDetails, labelsMatch }]); } }); + matchesPath.forEach(({ route: routeInPath, details, labelsMatch }) => { + const matchDetailsPath = new Map( + Array.from(details.entries()).map(([matcher, labels]) => [matcher, Object.fromEntries(labels)]) + ); + + const currentRouteInpath = resultPath.get(routeInPath.id); + if (currentRouteInpath) { + currentRouteInpath.push({ instance, matchDetails: matchDetailsPath, labelsMatch }); + } else { + resultPath.set(routeInPath.id, [{ instance, matchDetails: matchDetailsPath, labelsMatch }]); + } + }); }); - return result; + return { result: result, resultPath: resultPath }; }, }; diff --git a/public/app/features/alerting/unified/useRouteGroupsMatcher.ts b/public/app/features/alerting/unified/useRouteGroupsMatcher.ts index 36da0399851..7560ba95d30 100644 --- a/public/app/features/alerting/unified/useRouteGroupsMatcher.ts +++ b/public/app/features/alerting/unified/useRouteGroupsMatcher.ts @@ -98,7 +98,7 @@ export function useRouteGroupsMatcher() { const startTime = performance.now(); - const result = await routeMatcher.matchInstancesToRoute(rootRoute, instancesToMatch); + const { result, resultPath } = await routeMatcher.matchInstancesToRoute(rootRoute, instancesToMatch); const timeSpent = performance.now() - startTime; @@ -108,8 +108,7 @@ export function useRouteGroupsMatcher() { // Counting all nested routes might be too time-consuming, so we only count the first level topLevelRoutesCount: rootRoute.routes?.length.toString() ?? '0', }); - - return result; + return { result, resultPath }; }, [workerPreviewEnabled] ); diff --git a/public/app/features/alerting/unified/utils/notification-policies.test.ts b/public/app/features/alerting/unified/utils/notification-policies.test.ts index 5ad7dc27298..c3045dd492e 100644 --- a/public/app/features/alerting/unified/utils/notification-policies.test.ts +++ b/public/app/features/alerting/unified/utils/notification-policies.test.ts @@ -1,6 +1,6 @@ import { MatcherOperator, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; -import { findMatchingRoutes, normalizeRoute, getInheritedProperties } from './notification-policies'; +import { findMatchingRoutes, getInheritedProperties, normalizeRoute } from './notification-policies'; import 'core-js/stable/structured-clone'; @@ -39,19 +39,19 @@ describe('findMatchingRoutes', () => { }; it('should match root route with no matching labels', () => { - const matches = findMatchingRoutes(policies, []); + const { matchesResult: matches } = findMatchingRoutes(policies, []); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'ROOT'); }); it('should match parent route with no matching children', () => { - const matches = findMatchingRoutes(policies, [['team', 'operations']]); + const { matchesResult: matches } = findMatchingRoutes(policies, [['team', 'operations']]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'A'); }); it('should match child route of matching parent', () => { - const matches = findMatchingRoutes(policies, [ + const { matchesResult: matches } = findMatchingRoutes(policies, [ ['team', 'operations'], ['region', 'europe'], ]); @@ -60,7 +60,7 @@ describe('findMatchingRoutes', () => { }); it('should match simple policy', () => { - const matches = findMatchingRoutes(policies, [['foo', 'bar']]); + const { matchesResult: matches } = findMatchingRoutes(policies, [['foo', 'bar']]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'C'); }); @@ -71,7 +71,7 @@ describe('findMatchingRoutes', () => { routes: [CATCH_ALL_ROUTE, ...(policies.routes ?? [])], }; - const matches = findMatchingRoutes(policiesWithAll, []); + const { matchesResult: matches } = findMatchingRoutes(policiesWithAll, []); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'ALL'); }); @@ -88,7 +88,7 @@ describe('findMatchingRoutes', () => { ], }; - const matches = findMatchingRoutes(policiesWithAll, [['foo', 'bar']]); + const { matchesResult: matches } = findMatchingRoutes(policiesWithAll, [['foo', 'bar']]); expect(matches).toHaveLength(2); expect(matches[0].route).toHaveProperty('receiver', 'ALL'); expect(matches[1].route).toHaveProperty('receiver', 'C'); @@ -115,7 +115,7 @@ describe('findMatchingRoutes', () => { group_interval: '1m', }; - const matches = findMatchingRoutes(policies, [['foo', 'bar']]); + const { matchesResult: matches } = findMatchingRoutes(policies, [['foo', 'bar']]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'PARENT'); }); diff --git a/public/app/features/alerting/unified/utils/notification-policies.ts b/public/app/features/alerting/unified/utils/notification-policies.ts index 946f86f2941..c5100e82fb9 100644 --- a/public/app/features/alerting/unified/utils/notification-policies.ts +++ b/public/app/features/alerting/unified/utils/notification-policies.ts @@ -37,7 +37,7 @@ function isLabelMatch(matcher: ObjectMatcher, label: Label) { return matchFunction(labelValue, matcherValue); } -interface LabelMatchResult { +export interface LabelMatchResult { match: boolean; matchers: ObjectMatcher[]; } @@ -99,36 +99,60 @@ export interface RouteMatchResult { // If the current node is not a match, return nothing // const normalizedMatchers = normalizeMatchers(root); // Normalization should have happened earlier in the code -function findMatchingRoutes(root: T, labels: Label[]): Array> { - let matches: Array> = []; +function findMatchingRoutes( + mainRoot: T, + labels: Label[] +): { matchesResult: Array>; matchesPath: Array> } { + // ----------- recursive function to find matching routes + function findMatchingRoutesRecursive( + root: T | undefined, + labels: Label[], + matchesPathAcum: Array> + ): { matchesResult: Array>; matchesPath: Array> } { + let matches: Array> = []; + if (!root) { + return { matchesResult: [], matchesPath: matchesPathAcum }; + } + // If the current node is not a match, return nothing + const matchResult: MatchingResult = matchLabels(root.object_matchers ?? [], labels); + if (!matchResult.matches) { + return { matchesResult: [], matchesPath: matchesPathAcum }; + } + // If the current node matches, add current match to the path results and continue with the children + matchesPathAcum.push({ route: root, details: matchResult.details, labelsMatch: matchResult.labelsMatch }); + if (root.routes) { + for (let index = 0; index < root.routes.length; index++) { + let child = root.routes?.[index]; + let { matchesResult: matchingChildren, matchesPath: matchesPathInChild } = findMatchingRoutesRecursive( + child, + labels, + matchesPathAcum + ); + // TODO how do I solve this typescript thingy? It looks correct to me /shrug + // @ts-ignore + matches = matches.concat(matchingChildren); + // @ts-ignore + matchingChildren.length && matchesPathAcum.concat(matchesPathInChild); - // If the current node is not a match, return nothing - const matchResult = matchLabels(root.object_matchers ?? [], labels); - if (!matchResult.matches) { - return []; - } - - // If the current node matches, recurse through child nodes - if (root.routes) { - for (let index = 0; index < root.routes.length; index++) { - let child = root.routes[index]; - let matchingChildren = findMatchingRoutes(child, labels); - // TODO how do I solve this typescript thingy? It looks correct to me /shrug - // @ts-ignore - matches = matches.concat(matchingChildren); - // we have matching children and we don't want to continue, so break here - if (matchingChildren.length && !child.continue) { - break; + // 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 (matches.length === 0) { - matches.push({ route: root, details: matchResult.details, labelsMatch: matchResult.labelsMatch }); - } + // If no child nodes were matches, the current node itself is a match. + if (matches.length === 0) { + matches.push({ route: root, details: matchResult.details, labelsMatch: matchResult.labelsMatch }); + } - return matches; + const matchesResultUnique = [ + ...new Map(matches.map((matchInstance) => [JSON.stringify(matchInstance), matchInstance])).values(), + ]; + return { matchesResult: matchesResultUnique, matchesPath: matchesPathAcum }; + } + // ------------ call to the recursive function + return findMatchingRoutesRecursive(mainRoot, labels, []); } // This is a performance improvement to normalize matchers only once and use the normalized version later on @@ -162,7 +186,8 @@ function findMatchingAlertGroups( // 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); + const { matchesResult } = findMatchingRoutes(routeTree, labels); + return matchesResult.some((matchingRoute) => matchingRoute.route === route); }); // if the groups has any alerts left after matching, add it to the results