mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
2d10068714
commit
5942d8595d
@ -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());
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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]);
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user