From c724639f56b144d43b49f6615825dc05dd220e37 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 19 Apr 2022 08:28:05 -0500 Subject: [PATCH] Alerting: Display alert instances instead of alert rules when creating silence (#47396) (#47881) * modify matchers util for instances * filter alerts * change label to include instances * re add empty case (cherry picked from commit 4570615afc7c2e7964aa3b8cf15e1120ee5949eb) Co-authored-by: Peter Holmberg --- .../silences/MatchedSilencedRules.tsx | 52 +++++++++++-------- .../alerting/unified/utils/matchers.test.ts | 40 +++++++------- .../alerting/unified/utils/matchers.ts | 27 +++++----- 3 files changed, 66 insertions(+), 53 deletions(-) diff --git a/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx b/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx index e09479f3d2a..d464202f3a4 100644 --- a/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx +++ b/public/app/features/alerting/unified/components/silences/MatchedSilencedRules.tsx @@ -3,21 +3,22 @@ import { useFormContext } from 'react-hook-form'; import { useDebounce } from 'react-use'; import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; +import { dateTime, GrafanaTheme2 } from '@grafana/data'; import { Badge, useStyles2 } from '@grafana/ui'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; -import { RuleState } from '../rules/RuleState'; import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces'; -import { Annotation } from '../../utils/constants'; -import { findAlertRulesWithMatchers } from '../../utils/matchers'; +import { findAlertInstancesWithMatchers } from '../../utils/matchers'; import { fetchAllPromAndRulerRulesAction } from '../../state/actions'; -import { CombinedRule } from 'app/types/unified-alerting'; +import { Alert, AlertingRule } from 'app/types/unified-alerting'; import { MatcherFieldValue, SilenceFormFields } from '../../types/silence-form'; +import { isAlertingRule } from '../../utils/rules'; +import { AlertStateTag } from '../rules/AlertStateTag'; +import { AlertLabels } from '../AlertLabels'; type MatchedRulesTableItemProps = DynamicTableItemProps<{ - matchedRule: CombinedRule; + matchedInstance: Alert; }>; -type MatchedRulesTableColumnProps = DynamicTableColumnProps<{ matchedRule: CombinedRule }>; +type MatchedRulesTableColumnProps = DynamicTableColumnProps<{ matchedInstance: Alert }>; export const MatchedSilencedRules = () => { const [matchedAlertRules, setMatchedAlertRules] = useState([]); @@ -35,12 +36,15 @@ export const MatchedSilencedRules = () => { const combinedNamespaces = useCombinedRuleNamespaces(); useDebounce( () => { - const matchedRules = combinedNamespaces.flatMap((namespace) => { + const matchedInstances = combinedNamespaces.flatMap((namespace) => { return namespace.groups.flatMap((group) => { - return findAlertRulesWithMatchers(group.rules, matchers); + return group.rules + .map((combinedRule) => combinedRule.promRule) + .filter((rule): rule is AlertingRule => isAlertingRule(rule)) + .flatMap((rule) => findAlertInstancesWithMatchers(rule.alerts ?? [], matchers)); }); }); - setMatchedAlertRules(matchedRules); + setMatchedAlertRules(matchedInstances); }, 500, [combinedNamespaces, matchers] @@ -49,7 +53,7 @@ export const MatchedSilencedRules = () => { return (

- Affected alerts + Affected alert instances {matchedAlertRules.length > 0 ? ( ) : null} @@ -75,24 +79,30 @@ function useColumns(): MatchedRulesTableColumnProps[] { { id: 'state', label: 'State', - renderCell: function renderStateTag({ data: { matchedRule } }) { - return ; + renderCell: function renderStateTag({ data: { matchedInstance } }) { + return ; }, size: '160px', }, { - id: 'name', - label: 'Name', - renderCell: function renderName({ data: { matchedRule } }) { - return matchedRule.name; + id: 'labels', + label: 'Labels', + renderCell: function renderName({ data: { matchedInstance } }) { + return ; }, size: '250px', }, { - id: 'summary', - label: 'Summary', - renderCell: function renderSummary({ data: { matchedRule } }) { - return matchedRule.annotations[Annotation.summary] ?? ''; + id: 'created', + label: 'Created', + renderCell: function renderSummary({ data: { matchedInstance } }) { + return ( + <> + {matchedInstance.activeAt.startsWith('0001') + ? '-' + : dateTime(matchedInstance.activeAt).format('YYYY-MM-DD HH:mm:ss')} + + ); }, size: '400px', }, diff --git a/public/app/features/alerting/unified/utils/matchers.test.ts b/public/app/features/alerting/unified/utils/matchers.test.ts index 33ddec0bb14..cedddfe5e55 100644 --- a/public/app/features/alerting/unified/utils/matchers.test.ts +++ b/public/app/features/alerting/unified/utils/matchers.test.ts @@ -1,6 +1,6 @@ import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; -import { getMatcherQueryParams, findAlertRulesWithMatchers, parseQueryParamMatchers } from './matchers'; -import { mockCombinedRule } from '../mocks'; +import { getMatcherQueryParams, findAlertInstancesWithMatchers, parseQueryParamMatchers } from './matchers'; +import { mockPromAlert } from '../mocks'; describe('Unified Alerting matchers', () => { describe('getMatcherQueryParams tests', () => { @@ -39,42 +39,42 @@ describe('Unified Alerting matchers', () => { describe('matchLabelsToMatchers', () => { it('should match for equal', () => { const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.equal }]; - const rules = [mockCombinedRule({ labels: { foo: 'bar' } }), mockCombinedRule({ labels: { foo: 'baz' } })]; - const matchedRules = findAlertRulesWithMatchers(rules, matchers); + const alerts = [mockPromAlert({ labels: { foo: 'bar' } }), mockPromAlert({ labels: { foo: 'baz' } })]; + const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers); - expect(matchedRules).toHaveLength(1); + expect(matchedAlerts).toHaveLength(1); }); it('should match for not equal', () => { const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.notEqual }]; - const rules = [mockCombinedRule({ labels: { foo: 'bar' } }), mockCombinedRule({ labels: { foo: 'baz' } })]; + const alerts = [mockPromAlert({ labels: { foo: 'bar' } }), mockPromAlert({ labels: { foo: 'baz' } })]; - const matchedRules = findAlertRulesWithMatchers(rules, matchers); - expect(matchedRules).toHaveLength(1); + const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers); + expect(matchedAlerts).toHaveLength(1); }); it('should match for regex', () => { const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.regex }]; - const rules = [ - mockCombinedRule({ labels: { foo: 'bar' } }), - mockCombinedRule({ labels: { foo: 'baz' } }), - mockCombinedRule({ labels: { foo: 'bas' } }), + const alerts = [ + mockPromAlert({ labels: { foo: 'bar' } }), + mockPromAlert({ labels: { foo: 'baz' } }), + mockPromAlert({ labels: { foo: 'bas' } }), ]; - const matchedRules = findAlertRulesWithMatchers(rules, matchers); - expect(matchedRules).toHaveLength(1); + const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers); + expect(matchedAlerts).toHaveLength(1); }); it('should not match regex', () => { const matchers = [{ name: 'foo', value: 'bar', operator: MatcherOperator.notRegex }]; - const rules = [ - mockCombinedRule({ labels: { foo: 'bar' } }), - mockCombinedRule({ labels: { foo: 'baz' } }), - mockCombinedRule({ labels: { foo: 'bas' } }), + const alerts = [ + mockPromAlert({ labels: { foo: 'bar' } }), + mockPromAlert({ labels: { foo: 'baz' } }), + mockPromAlert({ labels: { foo: 'bas' } }), ]; - const matchedRules = findAlertRulesWithMatchers(rules, matchers); - expect(matchedRules).toHaveLength(2); + const matchedAlerts = findAlertInstancesWithMatchers(alerts, matchers); + expect(matchedAlerts).toHaveLength(2); }); }); }); diff --git a/public/app/features/alerting/unified/utils/matchers.ts b/public/app/features/alerting/unified/utils/matchers.ts index 6979f8764bf..619a370b40d 100644 --- a/public/app/features/alerting/unified/utils/matchers.ts +++ b/public/app/features/alerting/unified/utils/matchers.ts @@ -3,7 +3,7 @@ import { Labels } from '@grafana/data'; import { parseMatcher } from './alertmanager'; import { uniqBy } from 'lodash'; import { MatcherFieldValue } from '../types/silence-form'; -import { CombinedRule } from 'app/types/unified-alerting'; +import { Alert } from 'app/types/unified-alerting'; // Parses a list of entries like like "['foo=bar', 'baz=~bad*']" into SilenceMatcher[] export function parseQueryParamMatchers(matcherPairs: string[]): Matcher[] { @@ -27,16 +27,19 @@ export const getMatcherQueryParams = (labels: Labels) => { return matcherUrlParams; }; -interface MatchedRule { +interface MatchedInstance { id: string; data: { - matchedRule: CombinedRule; + matchedInstance: Alert; }; } -export const findAlertRulesWithMatchers = (rules: CombinedRule[], matchers: MatcherFieldValue[]): MatchedRule[] => { - const hasMatcher = (rule: CombinedRule, matcher: MatcherFieldValue) => { - return Object.entries(rule.labels).some(([key, value]) => { +export const findAlertInstancesWithMatchers = ( + instances: Alert[], + matchers: MatcherFieldValue[] +): MatchedInstance[] => { + const hasMatcher = (instance: Alert, matcher: MatcherFieldValue) => { + return Object.entries(instance.labels).some(([key, value]) => { if (!matcher.name || !matcher.value) { return false; } @@ -56,13 +59,13 @@ export const findAlertRulesWithMatchers = (rules: CombinedRule[], matchers: Matc }); }; - const filteredRules = rules.filter((rule) => { - return matchers.every((matcher) => hasMatcher(rule, matcher)); + const filteredInstances = instances.filter((instance) => { + return matchers.every((matcher) => hasMatcher(instance, matcher)); }); - const mappedRules = filteredRules.map((rule) => ({ - id: `${rule.namespace}-${rule.name}`, - data: { matchedRule: rule }, + const mappedInstances = filteredInstances.map((instance) => ({ + id: `${instance.activeAt}-${instance.value}`, + data: { matchedInstance: instance }, })); - return mappedRules; + return mappedInstances; };