Alerting: Display alert instances instead of alert rules when creating silence (#47396)

* modify matchers util for instances

* filter alerts

* change label to include instances

* re add empty case
This commit is contained in:
Peter Holmberg 2022-04-19 15:08:19 +02:00 committed by GitHub
parent bb5f77703c
commit 4570615afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 53 deletions

View File

@ -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<MatchedRulesTableItemProps[]>([]);
@ -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 (
<div>
<h4 className={styles.title}>
Affected alerts
Affected alert instances
{matchedAlertRules.length > 0 ? (
<Badge className={styles.badge} color="blue" text={matchedAlertRules.length} />
) : null}
@ -75,24 +79,30 @@ function useColumns(): MatchedRulesTableColumnProps[] {
{
id: 'state',
label: 'State',
renderCell: function renderStateTag({ data: { matchedRule } }) {
return <RuleState rule={matchedRule} isCreating={false} isDeleting={false} />;
renderCell: function renderStateTag({ data: { matchedInstance } }) {
return <AlertStateTag state={matchedInstance.state} />;
},
size: '160px',
},
{
id: 'name',
label: 'Name',
renderCell: function renderName({ data: { matchedRule } }) {
return matchedRule.name;
id: 'labels',
label: 'Labels',
renderCell: function renderName({ data: { matchedInstance } }) {
return <AlertLabels labels={matchedInstance.labels} />;
},
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',
},

View File

@ -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);
});
});
});

View File

@ -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;
};