mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: fix maching loki prom rules to rule rules, rename "Inactive" to "Normal" (#34111)
This commit is contained in:
parent
a71cebbcb1
commit
ddb2fc1ae6
@ -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]));
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
@ -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} />}
|
||||||
</>
|
</>
|
@ -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[];
|
||||||
|
@ -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 });
|
||||||
|
@ -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>
|
||||||
|
@ -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('');
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user