Alerting: Fix pending period for vanilla prom rules (#92728)

* Add support for reading pending period value from prom duration property

* Add tests

* Refactor the hook

* Remove unused import
This commit is contained in:
Konrad Lalik 2024-09-02 10:32:47 +02:00 committed by GitHub
parent 2d10068714
commit 5942d8595d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 99 additions and 15 deletions

View File

@ -12,18 +12,21 @@ import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
import {
getCloudRule,
getGrafanaRule,
getVanillaPromRule,
grantUserPermissions,
mockDataSource,
mockPluginLinkExtension,
mockPromAlertingRule,
} from '../../mocks';
import { grafanaRulerRule } from '../../mocks/grafanaRulerApi';
import { setupDataSources } from '../../testSetup/datasources';
import { Annotation } from '../../utils/constants';
import { DataSourceType } from '../../utils/datasource';
import * as ruleId from '../../utils/rule-id';
import { stringifyIdentifier } from '../../utils/rule-id';
import { AlertRuleProvider } from './RuleContext';
import RuleViewer from './RuleViewer';
import RuleViewer, { ActiveTab } from './RuleViewer';
// metadata and interactive elements
const ELEMENTS = {
@ -35,6 +38,12 @@ const ELEMENTS = {
evaluationInterval: (interval: string) => byText(`Every ${interval}`),
label: ([key, value]: [string, string]) => byRole('listitem', { name: `${key}: ${value}` }),
},
details: {
pendingPeriod: byText(/Pending period/i),
},
tabs: {
details: byRole('tab', { name: /Details/i }),
},
actions: {
edit: byRole('link', { name: 'Edit' }),
more: {
@ -259,13 +268,39 @@ describe('RuleViewer', () => {
await waitFor(() => expect(ELEMENTS.actions.more.pluginActions.declareIncident.get()).toBeEnabled());
});
});
describe('Vanilla Prometheus rule', () => {
const mockRule = getVanillaPromRule({
name: 'prom test alert',
annotations: { [Annotation.summary]: 'prom summary', [Annotation.runbookURL]: 'https://runbook.example.com' },
promRule: {
...mockPromAlertingRule(),
duration: 900, // 15 minutes
},
});
const mockRuleIdentifier = ruleId.fromCombinedRule('prometheus', mockRule);
it('should render pending period for vanilla Prometheus alert rule', async () => {
renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Details);
expect(screen.getByText('prom test alert')).toBeInTheDocument();
// One summary is rendered by the Title component, and the other by the DetailsTab component
expect(ELEMENTS.metadata.summary(mockRule.annotations[Annotation.summary]).getAll()).toHaveLength(2);
expect(within(ELEMENTS.details.pendingPeriod.get()).getByText(/15m/i)).toBeInTheDocument();
});
});
});
const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier) => {
const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier, tab: ActiveTab = ActiveTab.Query) => {
const path = `/alerting/${identifier.ruleSourceName}/${stringifyIdentifier(identifier)}/view?tab=${tab}`;
render(
<AlertRuleProvider identifier={identifier} rule={rule}>
<RuleViewer />
</AlertRuleProvider>
</AlertRuleProvider>,
{ historyOptions: { initialEntries: [path] } }
);
await waitFor(() => expect(ELEMENTS.loading.query()).not.toBeInTheDocument());

View File

@ -7,6 +7,7 @@ import { ClipboardButton, Stack, Text, TextLink, useStyles2 } from '@grafana/ui'
import { CombinedRule } from 'app/types/unified-alerting';
import { Annotations } from 'app/types/unified-alerting-dto';
import { usePendingPeriod } from '../../../hooks/rules/usePendingPeriod';
import { isGrafanaRulerRule, isRecordingRulerRule } from '../../../utils/rules';
import { MetaText } from '../../MetaText';
import { Tokenize } from '../../Tokenize';
@ -26,6 +27,8 @@ const Details = ({ rule }: DetailsProps) => {
let ruleType: RuleType;
const pendingPeriod = usePendingPeriod(rule);
if (isGrafanaRulerRule(rule.rulerRule)) {
ruleType = RuleType.GrafanaManagedAlertRule;
} else if (isRecordingRulerRule(rule.rulerRule)) {
@ -89,10 +92,10 @@ const Details = ({ rule }: DetailsProps) => {
)}
</MetaText>
<MetaText direction="column">
{!isRecordingRulerRule(rule.rulerRule) && (
{pendingPeriod && (
<>
Pending period
<Text color="primary">{rule.rulerRule?.for ?? '0s'}</Text>
<Text color="primary">{pendingPeriod}</Text>
</>
)}
</MetaText>

View File

@ -5,8 +5,9 @@ import { Tooltip, useStyles2 } from '@grafana/ui';
import { Time } from 'app/features/explore/Time';
import { CombinedRule } from 'app/types/unified-alerting';
import { usePendingPeriod } from '../../hooks/rules/usePendingPeriod';
import { useCleanAnnotations } from '../../utils/annotations';
import { isGrafanaRecordingRule, isRecordingRulerRule } from '../../utils/rules';
import { isGrafanaRecordingRule } from '../../utils/rules';
import { isNullDate } from '../../utils/time';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
@ -64,16 +65,12 @@ interface EvaluationBehaviorSummaryProps {
}
const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => {
let forDuration: string | undefined;
const every = rule.group.interval;
const lastEvaluation = rule.promRule?.lastEvaluation;
const lastEvaluationDuration = rule.promRule?.evaluationTime;
const metric = isGrafanaRecordingRule(rule.rulerRule) ? rule.rulerRule?.grafana_alert.record?.metric : undefined;
// recording rules don't have a for duration
if (!isRecordingRulerRule(rule.rulerRule)) {
forDuration = rule.rulerRule?.for ?? '0s';
}
const pendingPeriod = usePendingPeriod(rule);
return (
<>
@ -88,9 +85,11 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
</DetailsField>
)}
<DetailsField label="Pending period" horizontal={true}>
{forDuration}
</DetailsField>
{pendingPeriod && (
<DetailsField label="Pending period" horizontal={true}>
{pendingPeriod}
</DetailsField>
)}
{lastEvaluation && !isNullDate(lastEvaluation) && (
<DetailsField label="Last evaluation" horizontal={true}>

View File

@ -0,0 +1,9 @@
import { useMemo } from 'react';
import { CombinedRule } from 'app/types/unified-alerting';
import { getPendingPeriod } from '../../utils/rules';
export function usePendingPeriod(rule: CombinedRule): string | undefined {
return useMemo(() => getPendingPeriod(rule), [rule]);
}

View File

@ -785,6 +785,19 @@ export function getCloudRule(override?: Partial<CombinedRule>) {
});
}
export function getVanillaPromRule(override?: Partial<Omit<CombinedRule, 'rulerRule'>>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Prometheus',
rulesSource: mockDataSource(),
},
promRule: mockPromAlertingRule(),
rulerRule: undefined,
...override,
});
}
export function mockPluginLinkExtension(extension: Partial<PluginExtensionLink>): PluginExtensionLink {
return {
type: PluginExtensionTypes.link,

View File

@ -44,7 +44,7 @@ import { RULER_NOT_SUPPORTED_MSG } from './constants';
import { getRulesSourceName, isGrafanaRulesSource } from './datasource';
import { GRAFANA_ORIGIN_LABEL } from './labels';
import { AsyncRequestState } from './redux';
import { safeParsePrometheusDuration } from './time';
import { formatPrometheusDuration, safeParsePrometheusDuration } from './time';
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
@ -137,6 +137,26 @@ export function getRuleHealth(health: string): RuleHealth | undefined {
}
}
export function getPendingPeriod(rule: CombinedRule): string | undefined {
if (isRecordingRulerRule(rule.rulerRule) || isRecordingRule(rule.promRule)) {
return undefined;
}
// We prefer the for duration from the ruler rule because it is formatted as a duration string
// Prometheus duration is in seconds and we need to format it as a duration string
// Additionally, due to eventual consistency of the Prometheus endpoint the ruler data might be newer
if (isAlertingRulerRule(rule.rulerRule)) {
return rule.rulerRule.for;
}
if (isAlertingRule(rule.promRule)) {
const durationInMilliseconds = (rule.promRule.duration ?? 0) * 1000;
return formatPrometheusDuration(durationInMilliseconds);
}
return undefined;
}
export interface RulePluginOrigin {
pluginId: string;
}

View File

@ -50,6 +50,11 @@ export interface AlertingRule extends RuleBase {
};
state: PromAlertingRuleState;
type: PromRuleType.Alerting;
/**
* Pending period in seconds, aka for. 0 or undefined means no pending period
*/
duration?: number;
totals?: Partial<Record<Lowercase<GrafanaAlertState>, number>>;
totalsFiltered?: Partial<Record<Lowercase<GrafanaAlertState>, number>>;
activeAt?: string; // ISO timestamp