Alerting: Add keep_firing_for and Recovering state (#103248)

* add keep_firing_for and Recovering state

* prettier

* translations

* remove unused component/file

* fix tests

* fix test

* prettier

* fix tests

* revert changes in go.work.sum

* remove recovering from cloud rules filters

* prettier

* fix padding

* fix wrong move in import

* update text

* fix filtering states in alert list panel

* update translations

* betterer

* address feedback

* translations

* fix tests

* prettier and betterer

* update betterer.results

* update translations

* update snapshot

* add divider in the alert rule form

* address feedback

* Improve translations

* Update .betterer.results

---------

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
Sonia Aguilar
2025-04-07 17:13:04 +02:00
committed by GitHub
co-authored by Tom Ratcliffe
parent 09a7f9ba1c
commit 71c66acb2d
48 changed files with 317 additions and 94 deletions
+1 -11
View File
@@ -1349,9 +1349,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/rules/RuleConfigStatus.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rules/RuleDetails.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
@@ -1371,14 +1368,7 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rules/RulesGroup.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
@@ -7,6 +7,7 @@ export enum AlertState {
Alerting = 'alerting',
OK = 'ok',
Pending = 'pending',
Recovering = 'recovering',
Unknown = 'unknown',
}
@@ -147,6 +147,13 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
stateClass: 'alert-state-warning',
};
}
case 'recovering': {
return {
text: 'RECOVERING',
iconClass: 'hourglass',
stateClass: 'alert-state-warning',
};
}
case 'firing': {
return {
@@ -1,31 +0,0 @@
import { css } from '@emotion/css';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
type Props = {
status: PromAlertingRuleState | 'neutral';
};
export const StateColoredText = ({ children, status }: React.PropsWithChildren<Props>) => {
const styles = useStyles2(getStyles);
return <span className={styles[status]}>{children || status}</span>;
};
const getStyles = (theme: GrafanaTheme2) => ({
[PromAlertingRuleState.Inactive]: css({
color: theme.colors.success.text,
}),
[PromAlertingRuleState.Pending]: css({
color: theme.colors.warning.text,
}),
[PromAlertingRuleState.Firing]: css({
color: theme.colors.error.text,
}),
neutral: css({
color: theme.colors.text.secondary,
}),
});
@@ -1,11 +1,11 @@
import { render, screen, userEvent } from 'test/test-utils';
import { PendingPeriodQuickPick } from './PendingPeriodQuickPick';
import { DurationQuickPick } from './DurationQuickPick';
describe('PendingPeriodQuickPick', () => {
it('should render the correct default preset, set active element and allow selecting other options', async () => {
const onSelect = jest.fn();
render(<PendingPeriodQuickPick onSelect={onSelect} groupEvaluationInterval={'1m'} selectedPendingPeriod={'2m'} />);
render(<DurationQuickPick onSelect={onSelect} groupEvaluationInterval={'1m'} selectedDuration={'2m'} />);
const shouldHaveButtons = ['None', '1m', '2m', '3m', '4m', '5m'];
const shouldNotHaveButtons = ['0s', '10s', '6m'];
@@ -3,7 +3,7 @@ import { Button, Stack } from '@grafana/ui';
import { formatPrometheusDuration, safeParsePrometheusDuration } from '../../utils/time';
interface Props {
selectedPendingPeriod: string;
selectedDuration?: string;
groupEvaluationInterval: string;
onSelect: (interval: string) => void;
}
@@ -24,8 +24,8 @@ export function getPendingPeriodQuickOptions(groupEvaluationInterval: string): s
return options.map(formatPrometheusDuration);
}
export function PendingPeriodQuickPick({ selectedPendingPeriod, groupEvaluationInterval, onSelect }: Props) {
const isQuickSelectionActive = (duration: string) => selectedPendingPeriod === duration;
export function DurationQuickPick({ selectedDuration, groupEvaluationInterval, onSelect }: Props) {
const isQuickSelectionActive = (duration: string) => selectedDuration === duration;
const options = getPendingPeriodQuickOptions(groupEvaluationInterval);
@@ -8,6 +8,7 @@ import { selectors } from '@grafana/e2e-selectors';
import {
Box,
Button,
Divider,
Field,
Icon,
Input,
@@ -38,10 +39,10 @@ import { parsePrometheusDuration } from '../../utils/time';
import { CollapseToggle } from '../CollapseToggle';
import { ProvisioningBadge } from '../Provisioning';
import { DurationQuickPick } from './DurationQuickPick';
import { EvaluationGroupQuickPick } from './EvaluationGroupQuickPick';
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
import { NeedHelpInfo } from './NeedHelpInfo';
import { PendingPeriodQuickPick } from './PendingPeriodQuickPick';
import { RuleEditorSection } from './RuleEditorSection';
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
@@ -149,6 +150,7 @@ export function GrafanaEvaluationBehaviorStep({
'isPaused',
'folder',
'evaluateEvery',
'keepFiringFor',
]);
const isGrafanaAlertingRule = isGrafanaAlertingRuleByType(type);
@@ -293,6 +295,9 @@ export function GrafanaEvaluationBehaviorStep({
)}
{/* Show the pending period input only for Grafana alerting rules */}
{isGrafanaAlertingRule && <ForInput evaluateEvery={evaluateEvery} />}
<Divider />
{/*Show the keepFiringFor input only for Grafana alerting rules*/}
{isGrafanaAlertingRule && <KeepFiringFor evaluateEvery={evaluateEvery} />}
{existing && (
<Field htmlFor="pause-alert-switch">
@@ -542,8 +547,8 @@ export function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
>
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions(evaluateEvery))} />
</Field>
<PendingPeriodQuickPick
selectedPendingPeriod={currentPendingPeriod}
<DurationQuickPick
selectedDuration={currentPendingPeriod}
groupEvaluationInterval={evaluateEvery}
onSelect={setPendingPeriod}
/>
@@ -551,6 +556,52 @@ export function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
);
}
function KeepFiringFor({ evaluateEvery }: { evaluateEvery: string }) {
const styles = useStyles2(getStyles);
const {
register,
formState: { errors },
setValue,
watch,
} = useFormContext<RuleFormValues>();
const currentKeepFiringFor = watch('keepFiringFor');
const keepFiringForId = 'keep-firing-for-input';
const setKeepFiringFor = (keepFiringFor: string) => {
setValue('keepFiringFor', keepFiringFor);
};
return (
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
<Field
label={
<Label
htmlFor={keepFiringForId}
description={t(
'alerting.rule-form.evaluation-behaviour.keep-firing-for.label-description',
'Period during which the alert will continue to show up as firing even though the threshold condition is no longer breached. Selecting "None" means the alert will be back to normal immediately.'
)}
>
<Trans i18nKey="alerting.rule-form.evaluation-behaviour.keep-firing-for.label-text">Keep firing for</Trans>
</Label>
}
className={styles.inlineField}
error={errors.keepFiringFor?.message}
invalid={Boolean(errors.keepFiringFor?.message) ? true : undefined}
validationMessageHorizontalOverflow={true}
>
<Input id={keepFiringForId} width={8} {...register('keepFiringFor')} />
</Field>
<DurationQuickPick
selectedDuration={currentKeepFiringFor}
groupEvaluationInterval={evaluateEvery}
onSelect={setKeepFiringFor}
/>
</Stack>
);
}
function NeedHelpInfoForConfigureNoDataError() {
const docsLink =
'https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling';
@@ -563,11 +614,14 @@ function NeedHelpInfoForConfigureNoDataError() {
</Trans>
</Text>
<NeedHelpInfo
contentText="These settings can help mitigate temporary data source issues, preventing alerts from unintentionally firing due to lack of data, errors, or timeouts."
contentText={t(
'alerting.rule-form.evaluation-behaviour.info-help.content',
'These settings can help mitigate temporary data source issues, preventing alerts from unintentionally firing due to lack of data, errors, or timeouts.'
)}
externalLink={docsLink}
linkText={`Read more about this option`}
linkText={t('alerting.rule-form.evaluation-behaviour.info-help.link-text', `Read more about this option`)}
title={t(
'alerting.need-help-info-for-configure-no-data-error.title-configure-no-data-and-error-handling',
'alerting.rule-form.evaluation-behaviour.info-help.link-title',
'Configure no data and error handling'
)}
/>
@@ -614,8 +668,11 @@ function getDescription(isGrafanaRecordingRule: boolean) {
</>
}
externalLink={docsLink}
linkText={`Read about evaluation and alert states`}
title={t('alerting.get-description.title-alert-rule-evaluation', 'Alert rule evaluation')}
linkText={t(
'alerting.rule-form.evaluation-behaviour.info-help2.link-text',
`Read about evaluation and alert states`
)}
title={t('alerting.rule-form.evaluation-behaviour.info-help2.link-title', 'Alert rule evaluation')}
/>
</Stack>
);
@@ -10,6 +10,7 @@ import { getPayloadToExport } from './ModifyExportRuleForm';
const rule1 = mockRulerGrafanaRule(
{
for: '1m',
keep_firing_for: '1m',
labels: { severity: 'critical', region: 'region1' },
annotations: { [Annotation.summary]: 'This grafana rule1' },
},
@@ -19,6 +20,7 @@ const rule1 = mockRulerGrafanaRule(
const rule2 = mockRulerGrafanaRule(
{
for: '1m',
keep_firing_for: '1m',
labels: { severity: 'notcritical', region: 'region2' },
annotations: { [Annotation.summary]: 'This grafana rule2' },
},
@@ -28,6 +30,7 @@ const rule2 = mockRulerGrafanaRule(
const rule3 = mockRulerGrafanaRule(
{
for: '1m',
keep_firing_for: '1m',
labels: { severity: 'notcritical3', region: 'region3' },
annotations: { [Annotation.summary]: 'This grafana rule2' },
},
@@ -38,6 +41,7 @@ const rule4 = mockRulerGrafanaRecordingRule(
{
labels: { severity: 'notcritical4', region: 'region4' },
annotations: { [Annotation.summary]: 'This grafana rule4' },
keep_firing_for: '1m',
},
{ uid: 'uid-rule-4', title: 'Rule4', data: [] }
);
@@ -64,6 +68,7 @@ const formValuesForRule2Updated: RuleFormValues = {
name: 'Rule2 updated',
labels: [{ key: 'newLabel', value: 'newLabel' }],
annotations: [{ key: 'summary', value: 'This grafana rule2 updated' }],
keepFiringFor: '1m',
};
const formValuesForRecordingRule4Updated: RuleFormValues = {
...defaultValues,
@@ -114,6 +119,7 @@ const expectedModifiedRule2 = (uid: string) => ({
title: 'Rule2 updated',
uid: uid,
},
keep_firing_for: '1m',
labels: {
newLabel: 'newLabel',
},
@@ -141,7 +147,6 @@ const expectedModifiedRule4 = (uid: string) => ({
},
],
is_paused: false,
notification_settings: undefined,
record: {
metric: 'Rule4 updated',
from: 'A',
@@ -148,6 +148,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
},
"title": "my great new rule",
},
"keep_firing_for": "0s",
"labels": {},
},
],
@@ -319,6 +320,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"no_data_state": "NoData",
"title": "my great new rule",
},
"keep_firing_for": "0s",
"labels": {},
},
],
@@ -493,6 +495,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
},
"title": "my great new rule",
},
"keep_firing_for": "0s",
"labels": {},
},
],
@@ -664,6 +667,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
"no_data_state": "NoData",
"title": "my great new rule",
},
"keep_firing_for": "0s",
"labels": {},
},
],
@@ -838,6 +842,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch
},
"title": "my great new rule",
},
"keep_firing_for": "0s",
"labels": {},
},
],
@@ -472,6 +472,7 @@ export const calculateTotalInstances = (stats: CombinedRule['instanceTotals']) =
.pick([
AlertInstanceTotalState.Alerting,
AlertInstanceTotalState.Pending,
AlertInstanceTotalState.Recovering,
AlertInstanceTotalState.Normal,
AlertInstanceTotalState.NoData,
AlertInstanceTotalState.Error,
@@ -44,6 +44,10 @@ export const StateBadge = ({ state, health }: StateBadgeProps) => {
color = 'warning';
stateLabel = 'Pending';
break;
case PromAlertingRuleState.Recovering:
color = 'warning';
stateLabel = 'Recovering';
break;
}
// if the rule is in "error" health we don't really care about the state
@@ -43,6 +43,7 @@ export const Details = ({ rule }: DetailsProps) => {
const styles = useStyles2(getStyles);
const pendingPeriod = usePendingPeriod(rule);
const keepFiringFor = rulerRuleType.grafana.alertingRule(rule.rulerRule) ? rule.rulerRule.keep_firing_for : undefined;
let determinedRuleType: RuleType = RuleType.Unknown;
if (rulerRuleType.grafana.alertingRule(rule.rulerRule)) {
@@ -164,6 +165,13 @@ export const Details = ({ rule }: DetailsProps) => {
value={pendingPeriod}
/>
)}
{keepFiringFor && (
<DetailText
id="keep-firing-for"
label={t('alerting.alert.keep-firing-for', 'Keep firing for')}
value={keepFiringFor}
/>
)}
</DetailGroup>
{rulerRuleType.grafana.rule(rule.rulerRule) &&
@@ -6,7 +6,11 @@ import { Label, RadioButtonGroup, Tag, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
export type InstanceStateFilter = GrafanaAlertState | PromAlertingRuleState.Pending | PromAlertingRuleState.Firing;
export type InstanceStateFilter =
| GrafanaAlertState
| PromAlertingRuleState.Pending
| PromAlertingRuleState.Firing
| PromAlertingRuleState.Recovering;
interface Props {
className?: string;
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import * as React from 'react';
import { useMemo } from 'react';
import { PluginExtensionPoints, dateTime, findCommonLabels } from '@grafana/data';
import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting';
@@ -69,7 +69,7 @@ const columns: AlertTableColumnProps[] = [
alert: { state },
},
}) => <AlertStateTag state={state} />,
size: '80px',
size: '95px',
},
{
id: 'labels',
@@ -113,6 +113,7 @@ const FilterOptions = () => {
{ label: 'All', value: '*' },
{ label: 'Normal', value: 'normal' },
{ label: 'Pending', value: 'pending' },
{ label: 'Recovering', value: 'recovering' },
{ label: 'Firing', value: 'firing' },
]}
/>
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2, dateTime, dateTimeFormat } from '@grafana/data';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { Trans, t } from 'app/core/internationalization';
import { Time } from 'app/features/explore/Time';
import { CombinedRule } from 'app/types/unified-alerting';
@@ -78,6 +78,7 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
: undefined;
const pendingPeriod = usePendingPeriod(rule);
const keepFiringFor = rulerRuleType.grafana.alertingRule(rule.rulerRule) ? rule.rulerRule.keep_firing_for : undefined;
return (
<>
@@ -88,7 +89,9 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
)}
{every && (
<DetailsField label={t('alerting.evaluation-behavior-summary.label-evaluate', 'Evaluate')} horizontal={true}>
Every {every}
<Trans i18nKey="alerting.evaluation-behavior-summary.evaluate" values={{ every }}>
Every {{ every }}
</Trans>
</DetailsField>
)}
@@ -100,6 +103,11 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
{pendingPeriod}
</DetailsField>
)}
{keepFiringFor && (
<DetailsField label={t('alerting.rule-details.keep-firing-for', 'Keep firing for')} horizontal={true}>
{keepFiringFor}
</DetailsField>
)}
{lastEvaluation && !isNullDate(lastEvaluation) && (
<DetailsField
@@ -111,7 +119,11 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
content={`${dateTimeFormat(lastEvaluation, { format: 'YYYY-MM-DD HH:mm:ss' })}`}
theme="info"
>
<span>{`${dateTime(lastEvaluation).locale('en').fromNow(true)} ago`}</span>
<span>
{t('alerting.rule-details.last-evaluation-ago', '{{time}} ago', {
time: dateTime(lastEvaluation).locale('en').fromNow(true),
})}
</span>
</Tooltip>
</DetailsField>
)}
@@ -30,6 +30,7 @@ const ui = {
normal: byLabelText(/^Normal/),
alerting: byLabelText(/^Alerting/),
pending: byLabelText(/^Pending/),
recovering: byLabelText(/^Recovering/),
noData: byLabelText(/^NoData/),
error: byLabelText(/^Error/),
},
@@ -60,7 +61,7 @@ describe('RuleDetailsMatchingInstances', () => {
});
describe('Filtering', () => {
it('For Grafana Managed rules instances filter should contain five states', () => {
it('For Grafana Managed rules instances filter should contain six states', () => {
const rule = mockCombinedRule();
render(<RuleDetailsMatchingInstances rule={rule} enableFiltering />);
@@ -70,7 +71,7 @@ describe('RuleDetailsMatchingInstances', () => {
const stateButtons = ui.stateButton.getAll(stateFilter);
expect(stateButtons).toHaveLength(5);
expect(stateButtons).toHaveLength(6);
expect(ui.grafanaStateButton.normal.get(stateFilter)).toBeInTheDocument();
expect(ui.grafanaStateButton.alerting.get(stateFilter)).toBeInTheDocument();
@@ -86,6 +87,7 @@ describe('RuleDetailsMatchingInstances', () => {
mockPromAlert({ state: GrafanaAlertState.Normal }),
mockPromAlert({ state: GrafanaAlertState.Alerting }),
mockPromAlert({ state: GrafanaAlertState.Pending }),
mockPromAlert({ state: GrafanaAlertState.Recovering }),
mockPromAlert({ state: GrafanaAlertState.NoData }),
mockPromAlert({ state: GrafanaAlertState.Error }),
],
@@ -96,6 +98,7 @@ describe('RuleDetailsMatchingInstances', () => {
[GrafanaAlertState.Normal]: ui.grafanaStateButton.normal,
[GrafanaAlertState.Alerting]: ui.grafanaStateButton.alerting,
[GrafanaAlertState.Pending]: ui.grafanaStateButton.pending,
[GrafanaAlertState.Recovering]: ui.grafanaStateButton.recovering,
[GrafanaAlertState.NoData]: ui.grafanaStateButton.noData,
[GrafanaAlertState.Error]: ui.grafanaStateButton.error,
};
@@ -93,6 +93,7 @@ export function RuleDetailsMatchingInstances(props: Props) {
instanceTotals.alerting,
instanceTotals.inactive,
instanceTotals.pending,
instanceTotals.recovering,
instanceTotals.nodata,
]);
const hiddenInstancesCount = totalInstancesCount - visibleInstances.length;
@@ -34,6 +34,7 @@ export const RuleListStateView = ({ namespaces }: Props) => {
const result: GroupedRules = new Map([
[PromAlertingRuleState.Firing, []],
[PromAlertingRuleState.Pending, []],
[PromAlertingRuleState.Recovering, []],
[PromAlertingRuleState.Inactive, []],
]);
@@ -73,6 +74,7 @@ const STATE_TITLES: Record<PromAlertingRuleState, string> = {
[PromAlertingRuleState.Firing]: 'Firing',
[PromAlertingRuleState.Pending]: 'Pending',
[PromAlertingRuleState.Inactive]: 'Normal',
[PromAlertingRuleState.Recovering]: 'Recovering',
};
const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: CombinedRule[] }) => {
@@ -4,6 +4,7 @@ import * as React from 'react';
import { Fragment, useDeferredValue, useMemo } from 'react';
import { Badge, Stack } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import {
AlertGroupTotals,
AlertInstanceTotalState,
@@ -22,6 +23,7 @@ const emptyStats: Required<AlertGroupTotals> = {
alerting: 0,
[PromAlertingRuleState.Pending]: 0,
[PromAlertingRuleState.Inactive]: 0,
[PromAlertingRuleState.Recovering]: 0,
paused: 0,
error: 0,
nodata: 0,
@@ -81,7 +83,7 @@ function statsFromNamespaces(namespaces: CombinedRuleNamespace[]): AlertGroupTot
export function totalFromStats(stats: AlertGroupTotals): number {
// countable stats will pick only the states that indicate a single rule – health indicators like "error" and "nodata" should
// not be counted because they are already counted by their state
const countableStats = pick(stats, ['alerting', 'pending', 'inactive', 'recording']);
const countableStats = pick(stats, ['alerting', 'pending', 'inactive', 'recording', 'recovering']);
const total = sum(Object.values(countableStats));
return total;
@@ -117,41 +119,94 @@ export function getComponentsFromStats(
const statsComponents: React.ReactNode[] = [];
if (stats[AlertInstanceTotalState.Alerting]) {
statsComponents.push(<Badge color="red" key="firing" text={`${stats[AlertInstanceTotalState.Alerting]} firing`} />);
statsComponents.push(
<Badge
color="red"
key="firing"
text={t('alerting.rule-stats.firing', '{{alertingStats}} firing', {
alertingStats: stats[AlertInstanceTotalState.Alerting],
})}
/>
);
}
if (stats.error) {
statsComponents.push(<Badge color="red" key="errors" text={`${stats.error} ${pluralize('error', stats.error)}`} />);
statsComponents.push(
<Badge
color="red"
key="errors"
text={t('alerting.rule-stats.error', `{{count}} errors`, { count: stats.error })}
/>
);
}
if (stats.nodata) {
statsComponents.push(<Badge color="blue" key="nodata" text={`${stats.nodata} no data`} />);
statsComponents.push(
<Badge
color="blue"
key="nodata"
text={t('alerting.rule-stats.nodata', '{{nodataStats}} no data', { nodataStats: stats.nodata })}
/>
);
}
if (stats[AlertInstanceTotalState.Pending]) {
const pendingStats = stats[AlertInstanceTotalState.Pending];
statsComponents.push(
<Badge color={'orange'} key="pending" text={`${stats[AlertInstanceTotalState.Pending]} pending`} />
<Badge
color={'orange'}
key="pending"
text={t('alerting.rule-stats.pending', `{{pendingStats}} pending`, { pendingStats })}
/>
);
}
if (stats[AlertInstanceTotalState.Recovering]) {
const recoveringStats = stats[AlertInstanceTotalState.Recovering];
statsComponents.push(
<Badge
color={'orange'}
key="recovering"
text={t('alerting.rule-stats.recovering', `{{recoveringStats}} recovering`, { recoveringStats })}
/>
);
}
if (stats[AlertInstanceTotalState.Normal] && stats.paused) {
const normalStats = stats[AlertInstanceTotalState.Normal];
const pausedStats = stats.paused;
statsComponents.push(
<Badge
color="green"
key="paused"
text={`${stats[AlertInstanceTotalState.Normal]} normal (${stats.paused} paused)`}
text={t('alerting.rule-stats.paused', `{{normalStats}} normal ({{pausedStats}} paused)`, {
normalStats,
pausedStats,
})}
/>
);
}
if (stats[AlertInstanceTotalState.Normal] && !stats.paused) {
const normalStats = stats[AlertInstanceTotalState.Normal];
statsComponents.push(
<Badge color="green" key="inactive" text={`${stats[AlertInstanceTotalState.Normal]} normal`} />
<Badge
color="green"
key="inactive"
text={t('alerting.rule-stats.inactive', `{{normalStats}} normal`, { normalStats })}
/>
);
}
if (stats.recording) {
statsComponents.push(<Badge color="purple" key="recording" text={`${stats.recording} recording`} />);
const recordingStats = stats.recording;
statsComponents.push(
<Badge
color="purple"
key="recording"
text={t('alerting.rule-stats.recording', `{{recordingStats}} recording`, { recordingStats })}
/>
);
}
return statsComponents;
@@ -62,6 +62,7 @@ export const StateFilterValues = {
firing: 'Alerting',
normal: 'Normal',
pending: 'Pending',
recovering: 'Recovering',
} as const;
export const CentralAlertHistoryScene = () => {
@@ -86,7 +87,7 @@ export const CentralAlertHistoryScene = () => {
value: StateFilterValues.all,
label: 'End state:',
hide: VariableHide.dontHide,
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending}`,
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending},To Recovering : ${StateFilterValues.recovering}`,
});
//custom variable for filtering by the previous state
@@ -95,7 +96,7 @@ export const CentralAlertHistoryScene = () => {
value: StateFilterValues.all,
label: 'Start state:',
hide: VariableHide.dontHide,
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending}`,
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending},From Recovering : ${StateFilterValues.recovering}`,
});
return new EmbeddedScene({
@@ -349,6 +349,12 @@ export function EventState({ state, showLabel = false, addFilter, type }: EventS
tooltipContent: Boolean(reason) ? `Pending (${reason})` : 'Pending',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.pending">Pending</Trans>,
},
Recovering: {
iconName: 'circle',
iconColor: styles.warningColor,
tooltipContent: Boolean(reason) ? `Recovering (${reason})` : 'Recovering',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.recovering">Recovering</Trans>,
},
};
function onStateClick() {
addFilter('state', baseState, type === 'from' ? 'stateFrom' : 'stateTo');
@@ -193,7 +193,7 @@ function logRecordsToDataFrame(instanceLabels: string, records: LogRecord[]): Da
* The time field is the timestamp of the log record.
* The value field is the state of the log record.
* The state is converted to a string and color is assigned based on the state.
* The state can be Alerting, Pending, Normal, or NoData.
* The state can be Alerting, Pending, Recovering, Normal, or NoData.
*
* */
export function logRecordsToDataFrameForState(records: LogRecord[], theme: GrafanaTheme2): DataFrame {
@@ -235,6 +235,9 @@ export function logRecordsToDataFrameForState(records: LogRecord[], theme: Grafa
Pending: {
color: theme.colors.warning.main,
},
Recovering: {
color: theme.colors.warning.main,
},
Normal: {
color: theme.colors.success.main,
},
@@ -40,6 +40,7 @@ export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerP
legendItems={[
{ label: 'Normal', color: theme.colors.success.main, yAxis: 1 },
{ label: 'Pending', color: theme.colors.warning.main, yAxis: 1 },
{ label: 'Recovering', color: theme.colors.warning.main, yAxis: 1 },
{ label: 'Firing', color: theme.colors.error.main, yAxis: 1 },
{ label: 'No Data', color: theme.colors.info.main, yAxis: 1 },
{ label: 'Mixed', color: theme.colors.text.secondary, yAxis: 1 },
@@ -140,6 +140,9 @@ export function logRecordsToDataFrame(
Pending: {
color: theme.colors.warning.main,
},
Recovering: {
color: theme.colors.warning.main,
},
NoData: {
color: theme.colors.info.main,
},
@@ -77,6 +77,7 @@ const SERIES_COLORS = {
missed: 'red',
failed: 'red',
pending: 'yellow',
recovering: 'yellow',
nodata: 'blue',
'active evaluation': 'blue',
normal: 'green',
@@ -240,6 +241,12 @@ function getGrafanaManagedScenes() {
'The number of currently firing alert rule instances',
'alerting'
),
getInstanceStatByStatusScene(
cloudUsageDs,
'Recovering instances',
'The number of currently recovering alert rule instances',
'recovering'
),
getInstanceStatByStatusScene(
cloudUsageDs,
'Pending instances',
@@ -320,6 +320,7 @@ export function calculateRuleTotals(rule: Pick<AlertingRule, 'alerts' | 'totals'
return {
alerting: result[AlertInstanceTotalState.Alerting] || result.firing,
pending: result[AlertInstanceTotalState.Pending],
recovering: result[AlertInstanceTotalState.Recovering],
inactive: result[AlertInstanceTotalState.Normal],
nodata: result[AlertInstanceTotalState.NoData],
error: result[AlertInstanceTotalState.Error] || result.err || undefined, // Prometheus uses "err" instead of "error"
@@ -356,6 +357,7 @@ export function calculateGroupTotals(group: Pick<RuleGroup, 'rules' | 'totals'>)
nodata: countsByHealth.nodata,
inactive: countsByState[PromAlertingRuleState.Inactive],
pending: countsByState[PromAlertingRuleState.Pending],
recovering: countsByState[PromAlertingRuleState.Recovering],
recording: recordingCount,
};
}
@@ -51,6 +51,8 @@ export function getGrafanaInstancesByStateScene(datasource: DataSourceRef, panel
.overrideColor(overrideToFixedColor('normal'))
.matchFieldsWithName('pending')
.overrideColor(overrideToFixedColor('pending'))
.matchFieldsWithName('recovering')
.overrideColor(overrideToFixedColor('recovering'))
.matchFieldsWithName('error')
.overrideColor(overrideToFixedColor('error'))
.matchFieldsWithName('nodata')
@@ -7,7 +7,7 @@ export function getInstanceStatByStatusScene(
datasource: DataSourceRef,
panelTitle: string,
panelDescription: string,
status: 'alerting' | 'pending' | 'nodata' | 'normal' | 'error'
status: 'alerting' | 'pending' | 'nodata' | 'normal' | 'error' | 'recovering'
) {
const expr = INSTANCE_ID
? `sum by (state) (grafanacloud_grafana_instance_alerting_alerts{state="${status}", id="${INSTANCE_ID}"})`
@@ -147,6 +147,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
"no_data_state": "NoData",
"title": "my great new rule",
},
"keep_firing_for": "0s",
"labels": {},
},
],
@@ -254,6 +255,7 @@ exports[`RuleEditor grafana managed rules can restore grafana managed alert when
"no_data_state": "NoData",
"title": "Grafana-rule",
},
"keep_firing_for": "0",
"labels": {
"region": "nasa",
"severity": "critical",
@@ -29,6 +29,7 @@ import {
const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s');
const GROUP_EVALUATION_INTERVAL_LOWER_BOUND = safeParsePrometheusDuration('1m');
const GROUP_EVALUATION_INTERVAL_UPPER_BOUND = Infinity;
const KEEP_FIRING_FOR_DEFAULT = '0s';
export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration(
clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND)
@@ -53,6 +54,7 @@ export const getDefaultFormValues = (): RuleFormValues => {
noDataState: GrafanaAlertStateDecision.NoData,
execErrState: GrafanaAlertStateDecision.Error,
evaluateFor: DEFAULT_GROUP_EVALUATION_INTERVAL,
keepFiringFor: KEEP_FIRING_FOR_DEFAULT,
evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL,
manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false
contactPoints: {},
@@ -32,6 +32,7 @@ export const StateView = ({ namespaces }: Props) => {
const result: GroupedRules = new Map([
[PromAlertingRuleState.Firing, []],
[PromAlertingRuleState.Pending, []],
[PromAlertingRuleState.Recovering, []],
[PromAlertingRuleState.Inactive, []],
]);
@@ -68,6 +69,7 @@ const STATE_TITLES: Record<PromAlertingRuleState, string> = {
[PromAlertingRuleState.Firing]: 'Firing',
[PromAlertingRuleState.Pending]: 'Pending',
[PromAlertingRuleState.Inactive]: 'Normal',
[PromAlertingRuleState.Recovering]: 'Recovering',
};
const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: CombinedRule[] }) => {
@@ -27,12 +27,14 @@ export enum RuleOperation {
const icons: Record<PromAlertingRuleState, IconName> = {
[PromAlertingRuleState.Inactive]: 'check-circle',
[PromAlertingRuleState.Pending]: 'circle',
[PromAlertingRuleState.Recovering]: 'exclamation-circle',
[PromAlertingRuleState.Firing]: 'exclamation-circle',
};
const color: Record<PromAlertingRuleState, 'success' | 'error' | 'warning'> = {
[PromAlertingRuleState.Inactive]: 'success',
[PromAlertingRuleState.Pending]: 'warning',
[PromAlertingRuleState.Recovering]: 'warning',
[PromAlertingRuleState.Firing]: 'error',
};
@@ -40,6 +42,7 @@ const stateNames: Record<PromAlertingRuleState, string> = {
[PromAlertingRuleState.Inactive]: 'Normal',
[PromAlertingRuleState.Pending]: 'Pending',
[PromAlertingRuleState.Firing]: 'Firing',
[PromAlertingRuleState.Recovering]: 'Recovering',
};
const operationIcons: Record<RuleOperation, IconName> = {
@@ -49,6 +49,7 @@ export interface RuleFormValues {
folder: Folder | undefined;
evaluateEvery: string;
evaluateFor: string;
keepFiringFor?: string;
isPaused?: boolean;
manualRouting: boolean; // if true contactPoints are used. This field will not be used for saving the rule
contactPoints?: AlertManagerManualRouting;
@@ -14,6 +14,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu
"notification_settings": undefined,
"title": "",
},
"keep_firing_for": "0s",
"labels": {},
}
`;
@@ -66,6 +67,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range
"notification_settings": undefined,
"title": "",
},
"keep_firing_for": "0s",
"labels": {},
}
`;
@@ -206,7 +206,9 @@ const alertStateSortScore = {
[PromAlertingRuleState.Firing]: 1,
[GrafanaAlertState.Error]: 1,
[GrafanaAlertState.Pending]: 2,
[GrafanaAlertState.Recovering]: 2,
[PromAlertingRuleState.Pending]: 2,
[PromAlertingRuleState.Recovering]: 2,
[PromAlertingRuleState.Inactive]: 2,
[GrafanaAlertState.NoData]: 3,
[GrafanaAlertState.Normal]: 4,
@@ -141,6 +141,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl
noDataState,
execErrState,
evaluateFor,
keepFiringFor,
queries,
isPaused,
contactPoints,
@@ -183,6 +184,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl
// Alerting rule specific
for: evaluateFor,
keep_firing_for: keepFiringFor,
};
} else if (wantsRecordingRule) {
return {
@@ -299,6 +301,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
group: group.name,
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
evaluateFor: rule.for || '0',
keepFiringFor: rule.keep_firing_for || '0',
noDataState: ga.no_data_state,
execErrState: ga.exec_err_state,
queries: ga.data,
@@ -371,6 +374,7 @@ export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace:
const ga = rule.grafana_alert;
const duration = rule.for;
const keepFiringFor = rule.keep_firing_for;
const annotations = rule.annotations;
const labels = rule.labels;
@@ -403,6 +407,7 @@ export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace:
type: RuleFormType.grafana,
group: ga.rule_group,
evaluateFor: duration || '0',
keepFiringFor: keepFiringFor || '0',
noDataState: ga.no_data_state,
execErrState: ga.exec_err_state,
@@ -192,6 +192,18 @@ export function getPendingPeriod(rule: CombinedRule): string | undefined {
return undefined;
}
export function getKeepFiringfor(rule: CombinedRule): string | undefined {
if (rulerRuleType.any.recordingRule(rule.rulerRule)) {
return undefined;
}
if (isGrafanaAlertingRule(rule.rulerRule)) {
return rule.rulerRule.keep_firing_for;
}
return undefined;
}
export function getAnnotations(rule?: AlertingRule): Annotations {
return rule?.annotations ?? {};
}
@@ -288,17 +300,21 @@ const alertStateToStateMap: Record<PromAlertingRuleState | GrafanaAlertState | A
[PromAlertingRuleState.Inactive]: 'good',
[PromAlertingRuleState.Firing]: 'bad',
[PromAlertingRuleState.Pending]: 'warning',
[PromAlertingRuleState.Recovering]: 'warning',
[GrafanaAlertState.Alerting]: 'bad',
[GrafanaAlertState.Error]: 'bad',
[GrafanaAlertState.NoData]: 'info',
[GrafanaAlertState.Normal]: 'good',
[GrafanaAlertState.Pending]: 'warning',
[GrafanaAlertState.Recovering]: 'warning',
[AlertState.NoData]: 'info',
[AlertState.Paused]: 'warning',
[AlertState.Alerting]: 'bad',
[AlertState.OK]: 'good',
// AlertState.Pending is not included because the 'pending' value is already covered by `PromAlertingRuleState.Pending`
// [AlertState.Pending]: 'warning',
// same for AlertState.Recovering
// [AlertState.Recovering]: 'warning',
[AlertState.Unknown]: 'info',
};
@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css';
import { PanelData, GrafanaTheme2, PanelModel, LinkModel, AlertState, DataLink } from '@grafana/data';
import { Icon, PanelChrome, Tooltip, useStyles2, TimePickerTooltip } from '@grafana/ui';
import { AlertState, DataLink, GrafanaTheme2, LinkModel, PanelData, PanelModel } from '@grafana/data';
import { Icon, PanelChrome, TimePickerTooltip, Tooltip, useStyles2 } from '@grafana/ui';
import { PanelLinks } from '../PanelLinks';
@@ -31,7 +31,7 @@ export function PanelHeaderTitleItems(props: Props) {
<PanelChrome.TitleItem
className={cx({
[styles.ok]: alertState === AlertState.OK,
[styles.pending]: alertState === AlertState.Pending,
[styles.pending]: alertState === AlertState.Pending || alertState === AlertState.Recovering,
[styles.alerting]: alertState === AlertState.Alerting,
})}
>
@@ -304,7 +304,8 @@ function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: Combined
return (
(options.stateFilter.firing && alertingRule.state === PromAlertingRuleState.Firing) ||
(options.stateFilter.pending && alertingRule.state === PromAlertingRuleState.Pending) ||
(options.stateFilter.normal && alertingRule.state === PromAlertingRuleState.Inactive)
(options.stateFilter.normal && alertingRule.state === PromAlertingRuleState.Inactive) ||
(options.stateFilter.recovering && alertingRule.state === PromAlertingRuleState.Recovering)
);
});
@@ -109,7 +109,7 @@ const defaultOptions: UnifiedAlertListOptions = {
alertName: 'test',
showInstances: false,
folder: { id: 1, title: 'test folder' },
stateFilter: { firing: true, pending: false, noData: false, normal: true, error: false },
stateFilter: { firing: true, pending: false, noData: false, normal: true, error: false, recovering: false },
alertInstanceLabelFilter: '',
datasource: 'grafana',
viewMode: ViewMode.List,
@@ -8,7 +8,7 @@ import { GRAFANA_DATASOURCE_NAME } from '../../../features/alerting/unified/util
import { GroupBy } from './GroupByWithLoading';
import { UnifiedAlertListPanel } from './UnifiedAlertList';
import { UnifiedAlertListOptions, ViewMode, GroupMode, SortOrder } from './types';
import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types';
const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertListPanel).setPanelOptions((builder) => {
builder
@@ -168,6 +168,12 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
defaultValue: true,
category: ['Alert state filter'],
})
.addBooleanSwitch({
path: 'stateFilter.recovering',
name: 'Recovering',
defaultValue: true,
category: ['Alert state filter'],
})
.addBooleanSwitch({
path: 'stateFilter.noData',
name: 'No Data',
@@ -20,6 +20,7 @@ export interface StateFilter {
firing: boolean;
pending: boolean;
inactive?: boolean; // backwards compat
recovering: boolean;
noData: boolean;
normal: boolean;
error: boolean;
@@ -14,7 +14,7 @@ const defaultOption: UnifiedAlertListOptions = {
alertName: 'test',
showInstances: false,
folder: { id: 1, title: 'test folder' },
stateFilter: { firing: true, pending: true, noData: true, normal: true, error: true },
stateFilter: { firing: true, pending: true, noData: true, normal: true, error: true, recovering: false },
alertInstanceLabelFilter: '',
datasource: 'Alertmanager',
viewMode: ViewMode.List,
@@ -38,14 +38,14 @@ describe('filterAlerts', () => {
it('Filters by alert instance state ', () => {
const noNormalStateOptions = {
...defaultOption,
...{ stateFilter: { firing: true, pending: true, noData: true, normal: false, error: true } },
...{ stateFilter: { firing: true, pending: true, noData: true, normal: false, error: true, recovering: false } },
};
expect(filterAlerts(noNormalStateOptions, alerts).length).toBe(3);
const noErrorOrNormalStateOptions = {
...defaultOption,
...{ stateFilter: { firing: true, pending: true, noData: true, normal: false, error: false } },
...{ stateFilter: { firing: true, pending: true, noData: true, normal: false, error: false, recovering: false } },
};
expect(filterAlerts(noErrorOrNormalStateOptions, alerts).length).toBe(1);
@@ -64,7 +64,9 @@ describe('filterAlerts', () => {
it('Filters by alert instance state and label', () => {
const options = {
...defaultOption,
...{ stateFilter: { firing: false, pending: false, noData: false, normal: false, error: true } },
...{
stateFilter: { firing: false, pending: false, noData: false, normal: false, error: true, recovering: false },
},
...{ alertInstanceLabelFilter: '{severity=low}' },
};
const result = filterAlerts(options, alerts);
@@ -29,6 +29,9 @@ export function filterAlerts(
(hasAlertState(alert, GrafanaAlertState.Alerting) || hasAlertState(alert, PromAlertingRuleState.Firing))) ||
(stateFilter.pending &&
(hasAlertState(alert, GrafanaAlertState.Pending) || hasAlertState(alert, PromAlertingRuleState.Pending))) ||
(stateFilter.recovering &&
(hasAlertState(alert, GrafanaAlertState.Recovering) ||
hasAlertState(alert, PromAlertingRuleState.Recovering))) ||
(stateFilter.noData && hasAlertState(alert, GrafanaAlertState.NoData)) ||
(stateFilter.normal && hasAlertState(alert, GrafanaAlertState.Normal)) ||
(stateFilter.error && hasAlertState(alert, GrafanaAlertState.Error)) ||
+3
View File
@@ -12,12 +12,14 @@ export enum PromAlertingRuleState {
Firing = 'firing',
Inactive = 'inactive',
Pending = 'pending',
Recovering = 'recovering',
}
export enum GrafanaAlertState {
Normal = 'Normal',
Alerting = 'Alerting',
Pending = 'Pending',
Recovering = 'Recovering',
NoData = 'NoData',
Error = 'Error',
}
@@ -290,6 +292,7 @@ export type GrafanaRecordingRuleDefinition = GrafanaRuleDefinition & {
export interface RulerGrafanaRuleDTO<T = GrafanaRuleDefinition> {
grafana_alert: T;
for?: string;
keep_firing_for?: string;
annotations: Annotations;
labels: Labels;
}
+1
View File
@@ -143,6 +143,7 @@ export interface CombinedRule {
export enum AlertInstanceTotalState {
Alerting = 'alerting',
Pending = 'pending',
Recovering = 'recovering',
Normal = 'inactive',
NoData = 'nodata',
Error = 'error',
+29 -8
View File
@@ -300,6 +300,7 @@
"evaluation": "Evaluation",
"evaluation-paused": "Alert evaluation currently paused",
"evaluation-paused-description": "Notifications for this rule will not fire and no alert instances will be created until the rule is un-paused.",
"keep-firing-for": "Keep firing for",
"last-evaluated": "Last evaluated",
"last-evaluation-duration": "Last evaluation duration",
"last-updated-at": "Last updated at",
@@ -547,7 +548,8 @@
"error": "Error",
"no-data": "No data",
"normal": "Normal",
"pending": "Pending"
"pending": "Pending",
"recovering": "Recovering"
},
"state-transitions": "State transition",
"unknown-event-state": "Unknown",
@@ -805,6 +807,7 @@
"error": "1 error"
},
"evaluation-behavior-summary": {
"evaluate": "Every {{every}}",
"label-evaluate": "Evaluate",
"label-evaluation-time": "Evaluation time",
"label-last-evaluation": "Last evaluation",
@@ -893,9 +896,6 @@
"add-alert-data-to-payload": "Add alert data to payload",
"review-alert-payload": " Review alert data to add to the payload:"
},
"get-description": {
"title-alert-rule-evaluation": "Alert rule evaluation"
},
"get-preview-results": {
"title-error": "Error"
},
@@ -1285,9 +1285,6 @@
"need-help-info": {
"need-help": "Need help?"
},
"need-help-info-for-configure-no-data-error": {
"title-configure-no-data-and-error-handling": "Configure no data and error handling"
},
"need-help-info-for-contactpoint": {
"title-notify-by-selecting-a-contact-point": "Notify by selecting a contact point"
},
@@ -1560,8 +1557,10 @@
"title-view": "View"
},
"rule-details": {
"keep-firing-for": "Keep firing for",
"label-instances": "Instances",
"label-labels": "Labels"
"label-labels": "Labels",
"last-evaluation-ago": "{{time}} ago"
},
"rule-details-buttons": {
"go-to-dashboard": "Go to dashboard",
@@ -1617,8 +1616,19 @@
"text": "Define how the alert rule is evaluated."
},
"info-help": {
"content": "These settings can help mitigate temporary data source issues, preventing alerts from unintentionally firing due to lack of data, errors, or timeouts.",
"link-text": "Read more about this option",
"link-title": "Configure no data and error handling",
"text": "Define the alert behavior when the evaluation fails or the query returns no data."
},
"info-help2": {
"link-text": "Read about evaluation and alert states",
"link-title": "Alert rule evaluation"
},
"keep-firing-for": {
"label-description": "Period during which the alert will continue to show up as firing even though the threshold condition is no longer breached. Selecting \"None\" means the alert will be back to normal immediately.",
"label-text": "Keep firing for"
},
"pending-period": "Pending period"
},
"evaluation-behaviour-description1": "Evaluation groups are containers for evaluating alert and recording rules.",
@@ -1737,6 +1747,17 @@
"paused": "Paused",
"recording-rule": "Recording rule"
},
"rule-stats": {
"error_one": "{{count}} error",
"error_other": "{{count}} errors",
"firing": "{{alertingStats}} firing",
"inactive": "{{normalStats}} normal",
"nodata": "{{nodataStats}} no data",
"paused": "{{normalStats}} normal ({{pausedStats}} paused)",
"pending": "{{pendingStats}} pending",
"recording": "{{recordingStats}} recording",
"recovering": "{{recoveringStats}} recovering"
},
"rule-view": {
"query": {
"datasources-na": {