Alerting: fix maching loki prom rules to rule rules, rename "Inactive" to "Normal" (#34111)

This commit is contained in:
Domas 2021-05-14 13:41:13 +03:00 committed by GitHub
parent a71cebbcb1
commit ddb2fc1ae6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 66 additions and 22 deletions

View File

@ -244,16 +244,16 @@ describe('RuleList', () => {
let ruleRows = table.querySelectorAll<HTMLTableRowElement>(':scope > tbody > tr'); let ruleRows = table.querySelectorAll<HTMLTableRowElement>(':scope > tbody > tr');
expect(ruleRows).toHaveLength(4); expect(ruleRows).toHaveLength(4);
expect(ruleRows[0]).toHaveTextContent('n/a'); expect(ruleRows[0]).toHaveTextContent('Recording rule');
expect(ruleRows[0]).toHaveTextContent('recordingrule'); expect(ruleRows[0]).toHaveTextContent('recordingrule');
expect(ruleRows[1]).toHaveTextContent('firing'); expect(ruleRows[1]).toHaveTextContent('Firing');
expect(ruleRows[1]).toHaveTextContent('alertingrule'); expect(ruleRows[1]).toHaveTextContent('alertingrule');
expect(ruleRows[2]).toHaveTextContent('pending'); expect(ruleRows[2]).toHaveTextContent('Pending');
expect(ruleRows[2]).toHaveTextContent('p-rule'); expect(ruleRows[2]).toHaveTextContent('p-rule');
expect(ruleRows[3]).toHaveTextContent('inactive'); expect(ruleRows[3]).toHaveTextContent('Normal');
expect(ruleRows[3]).toHaveTextContent('i-rule'); expect(ruleRows[3]).toHaveTextContent('i-rule');
expect(byText('Labels').query()).not.toBeInTheDocument(); expect(byText('Labels').query()).not.toBeInTheDocument();
@ -277,8 +277,8 @@ describe('RuleList', () => {
let instanceRows = instancesTable?.querySelectorAll<HTMLTableRowElement>(':scope > tbody > tr'); let instanceRows = instancesTable?.querySelectorAll<HTMLTableRowElement>(':scope > tbody > tr');
expect(instanceRows).toHaveLength(2); expect(instanceRows).toHaveLength(2);
expect(instanceRows![0]).toHaveTextContent('firingfoo=barseverity=warning2021-03-18 13:47:05'); expect(instanceRows![0]).toHaveTextContent('Firingfoo=barseverity=warning2021-03-18 13:47:05');
expect(instanceRows![1]).toHaveTextContent('firingfoo=bazseverity=error2021-03-18 13:47:05'); expect(instanceRows![1]).toHaveTextContent('Firingfoo=bazseverity=error2021-03-18 13:47:05');
// expand details of an instance // expand details of an instance
userEvent.click(ui.alertCollapseToggle.get(instanceRows![0])); userEvent.click(ui.alertCollapseToggle.get(instanceRows![0]));

View File

@ -1,5 +1,6 @@
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { alertStateToReadable } from '../../utils/rules';
import { State, StateTag } from '../StateTag'; import { State, StateTag } from '../StateTag';
const alertStateToState: Record<PromAlertingRuleState | GrafanaAlertState, State> = { const alertStateToState: Record<PromAlertingRuleState | GrafanaAlertState, State> = {
@ -17,4 +18,6 @@ interface Props {
state: PromAlertingRuleState | GrafanaAlertState; state: PromAlertingRuleState | GrafanaAlertState;
} }
export const AlertStateTag: FC<Props> = ({ state }) => <StateTag state={alertStateToState[state]}>{state}</StateTag>; export const AlertStateTag: FC<Props> = ({ state }) => (
<StateTag state={alertStateToState[state]}>{alertStateToReadable(state)}</StateTag>
);

View File

@ -3,8 +3,8 @@ import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '@grafana/ui'; import { useStyles } from '@grafana/ui';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { capitalize } from 'lodash';
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { alertStateToReadable } from '../../utils/rules';
import { CollapseToggle } from '../CollapseToggle'; import { CollapseToggle } from '../CollapseToggle';
import { RulesTable } from './RulesTable'; import { RulesTable } from './RulesTable';
@ -26,7 +26,7 @@ export const RuleListStateSection: FC<Props> = ({ rules, state, defaultCollapsed
isCollapsed={collapsed} isCollapsed={collapsed}
onToggle={() => setCollapsed(!collapsed)} onToggle={() => setCollapsed(!collapsed)}
/> />
{capitalize(state)} ({rules.length}) {alertStateToReadable(state)} ({rules.length})
</h4> </h4>
{!collapsed && <RulesTable rules={rules} showGroupColumn={true} />} {!collapsed && <RulesTable rules={rules} showGroupColumn={true} />}
</> </>

View File

@ -4,7 +4,7 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { getFiltersFromUrlParams } from '../../utils/misc'; import { getFiltersFromUrlParams } from '../../utils/misc';
import { isAlertingRule } from '../../utils/rules'; import { isAlertingRule } from '../../utils/rules';
import { RuleListStateSection } from './RuleListSateSection'; import { RuleListStateSection } from './RuleListStateSection';
interface Props { interface Props {
namespaces: CombinedRuleNamespace[]; namespaces: CombinedRuleNamespace[];

View File

@ -8,6 +8,7 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { getFiltersFromUrlParams } from '../../utils/misc'; import { getFiltersFromUrlParams } from '../../utils/misc';
import { DataSourcePicker } from '@grafana/runtime'; import { DataSourcePicker } from '@grafana/runtime';
import { alertStateToReadable } from '../../utils/rules';
const RulesFilter = () => { const RulesFilter = () => {
const [queryParams, setQueryParams] = useQueryParams(); const [queryParams, setQueryParams] = useQueryParams();
@ -19,7 +20,10 @@ const RulesFilter = () => {
const { dataSource, alertState, queryString } = getFiltersFromUrlParams(queryParams); const { dataSource, alertState, queryString } = getFiltersFromUrlParams(queryParams);
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const stateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({ label: key, value })); const stateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({
label: alertStateToReadable(value),
value,
}));
const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings) => { const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings) => {
setQueryParams({ dataSource: dataSourceValue.name }); setQueryParams({ dataSource: dataSourceValue.name });

View File

@ -1,7 +1,7 @@
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { ConfirmModal, useStyles2 } from '@grafana/ui'; import { ConfirmModal, useStyles2 } from '@grafana/ui';
import React, { FC, Fragment, useState } from 'react'; import React, { FC, Fragment, useState } from 'react';
import { getRuleIdentifier, isAlertingRule, stringifyRuleIdentifier } from '../../utils/rules'; import { getRuleIdentifier, isAlertingRule, isRecordingRule, stringifyRuleIdentifier } from '../../utils/rules';
import { CollapseToggle } from '../CollapseToggle'; import { CollapseToggle } from '../CollapseToggle';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { RuleDetails } from './RuleDetails'; import { RuleDetails } from './RuleDetails';
@ -126,7 +126,15 @@ export const RulesTable: FC<Props> = ({
data-testid="rule-collapse-toggle" data-testid="rule-collapse-toggle"
/> />
</td> </td>
<td>{promRule && isAlertingRule(promRule) ? <AlertStateTag state={promRule.state} /> : 'n/a'}</td> <td>
{promRule && isAlertingRule(promRule) ? (
<AlertStateTag state={promRule.state} />
) : promRule && isRecordingRule(promRule) ? (
'Recording rule'
) : (
'n/a'
)}
</td>
<td>{rule.name}</td> <td>{rule.name}</td>
{showGroupColumn && ( {showGroupColumn && (
<td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td> <td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td>

View File

@ -164,23 +164,42 @@ function rulerRuleToCombinedRule(
}; };
} }
// find existing rule in group that matches the given prom rule
function getExistingRuleInGroup( function getExistingRuleInGroup(
rule: Rule, rule: Rule,
group: CombinedRuleGroup, group: CombinedRuleGroup,
rulesSource: RulesSource rulesSource: RulesSource
): CombinedRule | undefined { ): CombinedRule | undefined {
return isGrafanaRulesSource(rulesSource) if (isGrafanaRulesSource(rulesSource)) {
? group!.rules.find((existingRule) => existingRule.name === rule.name) // assume grafana groups have only the one rule. check name anyway because paranoid // assume grafana groups have only the one rule. check name anyway because paranoid
: group!.rules.find((existingRule) => { return group!.rules.find((existingRule) => existingRule.name === rule.name);
return !existingRule.promRule && isCombinedRuleEqualToPromRule(existingRule, rule); }
}); return (
// try finding a rule that matches name, labels, annotations and query
group!.rules.find(
(existingRule) => !existingRule.promRule && isCombinedRuleEqualToPromRule(existingRule, rule, true)
) ??
// if that fails, try finding a rule that only matches name, labels and annotations.
// loki & prom can sometimes modify the query so it doesnt match, eg `2 > 1` becomes `1`
group!.rules.find(
(existingRule) => !existingRule.promRule && isCombinedRuleEqualToPromRule(existingRule, rule, false)
)
);
} }
function isCombinedRuleEqualToPromRule(combinedRule: CombinedRule, rule: Rule): boolean { function isCombinedRuleEqualToPromRule(combinedRule: CombinedRule, rule: Rule, checkQuery = true): boolean {
if (combinedRule.name === rule.name) { if (combinedRule.name === rule.name) {
return ( return (
JSON.stringify([hashQuery(combinedRule.query), combinedRule.labels, combinedRule.annotations]) === JSON.stringify([
JSON.stringify([hashQuery(rule.query), rule.labels || {}, isAlertingRule(rule) ? rule.annotations || {} : {}]) checkQuery ? hashQuery(combinedRule.query) : '',
combinedRule.labels,
combinedRule.annotations,
]) ===
JSON.stringify([
checkQuery ? hashQuery(rule.query) : '',
rule.labels || {},
isAlertingRule(rule) ? rule.annotations || {} : {},
])
); );
} }
return false; return false;
@ -194,6 +213,6 @@ function hashQuery(query: string) {
} }
// whitespace could be added or removed // whitespace could be added or removed
query = query.replace(/\s|\n/g, ''); query = query.replace(/\s|\n/g, '');
// labels matchers can be reordered, so sort the enitre string, esentially comparing just hte character counts // labels matchers can be reordered, so sort the enitre string, esentially comparing just the character counts
return query.split('').sort().join(''); return query.split('').sort().join('');
} }

View File

@ -1,6 +1,8 @@
import { import {
Annotations, Annotations,
GrafanaAlertState,
Labels, Labels,
PromAlertingRuleState,
PromRuleType, PromRuleType,
RulerAlertingRuleDTO, RulerAlertingRuleDTO,
RulerGrafanaRuleDTO, RulerGrafanaRuleDTO,
@ -20,6 +22,7 @@ import {
import { AsyncRequestState } from './redux'; import { AsyncRequestState } from './redux';
import { RULER_NOT_SUPPORTED_MSG } from './constants'; import { RULER_NOT_SUPPORTED_MSG } from './constants';
import { hash } from './misc'; import { hash } from './misc';
import { capitalize } from 'lodash';
export function isAlertingRule(rule: Rule): rule is AlertingRule { export function isAlertingRule(rule: Rule): rule is AlertingRule {
return rule.type === PromRuleType.Alerting; return rule.type === PromRuleType.Alerting;
@ -132,3 +135,10 @@ export function ruleWithLocationToRuleIdentifier(ruleWithLocation: RuleWithLocat
ruleWithLocation.rule ruleWithLocation.rule
); );
} }
export function alertStateToReadable(state: PromAlertingRuleState | GrafanaAlertState): string {
if (state === PromAlertingRuleState.Inactive) {
return 'Normal';
}
return capitalize(state);
}