From 71c66acb2d803c7c747928dd87131352d87dc3cb Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:13:04 +0200 Subject: [PATCH] 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 --- .betterer.results | 12 +-- packages/grafana-data/src/types/alerts.ts | 1 + .../app/features/alerting/state/alertDef.ts | 7 ++ .../unified/components/StateColoredText.tsx | 31 -------- ...ck.test.tsx => DurationQuickPick.test.tsx} | 4 +- ...iodQuickPick.tsx => DurationQuickPick.tsx} | 6 +- .../rule-editor/GrafanaEvaluationBehavior.tsx | 73 +++++++++++++++++-- .../getPayloadToExport.test.ts | 7 +- .../SimplifiedRuleEditor.test.tsx.snap | 5 ++ .../components/rule-viewer/RuleViewer.tsx | 1 + .../components/rule-viewer/StateBadges.tsx | 4 + .../components/rule-viewer/tabs/Details.tsx | 8 ++ .../rules/AlertInstanceStateFilter.tsx | 6 +- .../components/rules/AlertInstancesTable.tsx | 4 +- .../rules/Filter/RulesFilter.v2.tsx | 1 + .../unified/components/rules/RuleDetails.tsx | 18 ++++- .../RuleDetailsMatchingInstances.test.tsx | 7 +- .../rules/RuleDetailsMatchingInstances.tsx | 1 + .../components/rules/RuleListStateView.tsx | 2 + .../unified/components/rules/RuleStats.tsx | 71 ++++++++++++++++-- .../CentralAlertHistoryScene.tsx | 5 +- .../EventListSceneObject.tsx | 6 ++ .../rules/central-state-history/utils.ts | 5 +- .../rules/state-history/LogTimelineViewer.tsx | 1 + .../state-history/useRuleHistoryRecords.tsx | 3 + .../alerting/unified/home/Insights.tsx | 7 ++ .../hooks/useCombinedRuleNamespaces.ts | 2 + .../insights/grafana/AlertsByStateScene.tsx | 2 + .../insights/grafana/InstanceStatusScene.tsx | 2 +- .../RuleEditorGrafanaRules.test.tsx.snap | 2 + .../unified/rule-editor/formDefaults.ts | 2 + .../alerting/unified/rule-list/StateView.tsx | 2 + .../rule-list/components/RuleListIcon.tsx | 3 + .../alerting/unified/types/rule-form.ts | 1 + .../__snapshots__/rule-form.test.ts.snap | 2 + .../features/alerting/unified/utils/misc.ts | 2 + .../alerting/unified/utils/rule-form.ts | 5 ++ .../features/alerting/unified/utils/rules.ts | 16 ++++ .../PanelHeader/PanelHeaderTitleItems.tsx | 6 +- .../panel/alertlist/UnifiedAlertList.tsx | 3 +- .../panel/alertlist/UnifiedalertList.test.tsx | 2 +- public/app/plugins/panel/alertlist/module.tsx | 8 +- public/app/plugins/panel/alertlist/types.ts | 1 + .../app/plugins/panel/alertlist/util.test.tsx | 10 ++- public/app/plugins/panel/alertlist/util.ts | 3 + public/app/types/unified-alerting-dto.ts | 3 + public/app/types/unified-alerting.ts | 1 + public/locales/en-US/grafana.json | 37 ++++++++-- 48 files changed, 317 insertions(+), 94 deletions(-) delete mode 100644 public/app/features/alerting/unified/components/StateColoredText.tsx rename public/app/features/alerting/unified/components/rule-editor/{PendingPeriodQuickPick.test.tsx => DurationQuickPick.test.tsx} (83%) rename public/app/features/alerting/unified/components/rule-editor/{PendingPeriodQuickPick.tsx => DurationQuickPick.tsx} (85%) diff --git a/.betterer.results b/.betterer.results index 0e94061d6ec..b6061680598 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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 ", "0"] ], - "public/app/features/alerting/unified/components/rules/RuleDetails.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -1371,14 +1368,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/alerting/unified/components/rules/RulesGroup.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], diff --git a/packages/grafana-data/src/types/alerts.ts b/packages/grafana-data/src/types/alerts.ts index 497562b47e1..a7214a0c5be 100644 --- a/packages/grafana-data/src/types/alerts.ts +++ b/packages/grafana-data/src/types/alerts.ts @@ -7,6 +7,7 @@ export enum AlertState { Alerting = 'alerting', OK = 'ok', Pending = 'pending', + Recovering = 'recovering', Unknown = 'unknown', } diff --git a/public/app/features/alerting/state/alertDef.ts b/public/app/features/alerting/state/alertDef.ts index cdcdcbda7c1..efa0180968e 100644 --- a/public/app/features/alerting/state/alertDef.ts +++ b/public/app/features/alerting/state/alertDef.ts @@ -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 { diff --git a/public/app/features/alerting/unified/components/StateColoredText.tsx b/public/app/features/alerting/unified/components/StateColoredText.tsx deleted file mode 100644 index 6831ff39189..00000000000 --- a/public/app/features/alerting/unified/components/StateColoredText.tsx +++ /dev/null @@ -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) => { - const styles = useStyles2(getStyles); - - return {children || status}; -}; - -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, - }), -}); diff --git a/public/app/features/alerting/unified/components/rule-editor/PendingPeriodQuickPick.test.tsx b/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.test.tsx similarity index 83% rename from public/app/features/alerting/unified/components/rule-editor/PendingPeriodQuickPick.test.tsx rename to public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.test.tsx index fa7bbac5699..7ee301d8ebb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/PendingPeriodQuickPick.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.test.tsx @@ -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(); + render(); const shouldHaveButtons = ['None', '1m', '2m', '3m', '4m', '5m']; const shouldNotHaveButtons = ['0s', '10s', '6m']; diff --git a/public/app/features/alerting/unified/components/rule-editor/PendingPeriodQuickPick.tsx b/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.tsx similarity index 85% rename from public/app/features/alerting/unified/components/rule-editor/PendingPeriodQuickPick.tsx rename to public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.tsx index 5ee59437c06..65912a2f23a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/PendingPeriodQuickPick.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.tsx @@ -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); diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index ad7e0c03729..5adcd6290ba 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -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 && } + + {/*Show the keepFiringFor input only for Grafana alerting rules*/} + {isGrafanaAlertingRule && } {existing && ( @@ -542,8 +547,8 @@ export function ForInput({ evaluateEvery }: { evaluateEvery: string }) { > - @@ -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(); + + const currentKeepFiringFor = watch('keepFiringFor'); + const keepFiringForId = 'keep-firing-for-input'; + + const setKeepFiringFor = (keepFiringFor: string) => { + setValue('keepFiringFor', keepFiringFor); + }; + + return ( + + + Keep firing for + + } + className={styles.inlineField} + error={errors.keepFiringFor?.message} + invalid={Boolean(errors.keepFiringFor?.message) ? true : undefined} + validationMessageHorizontalOverflow={true} + > + + + + + ); +} + 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() { @@ -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')} /> ); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts index 2a1e67855e3..e56320efaf0 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts @@ -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', diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap index 3cee4d53070..f0d3bd665fd 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap @@ -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": {}, }, ], diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index 14f61257fe9..095e893d40f 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -472,6 +472,7 @@ export const calculateTotalInstances = (stats: CombinedRule['instanceTotals']) = .pick([ AlertInstanceTotalState.Alerting, AlertInstanceTotalState.Pending, + AlertInstanceTotalState.Recovering, AlertInstanceTotalState.Normal, AlertInstanceTotalState.NoData, AlertInstanceTotalState.Error, diff --git a/public/app/features/alerting/unified/components/rule-viewer/StateBadges.tsx b/public/app/features/alerting/unified/components/rule-viewer/StateBadges.tsx index a2c620d0659..4dafeb52d17 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/StateBadges.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/StateBadges.tsx @@ -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 diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx index 7e87437d407..993dd6958b9 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx @@ -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 && ( + + )} {rulerRuleType.grafana.rule(rule.rulerRule) && diff --git a/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx index 5f5ba702257..31b5cd4b9a1 100644 --- a/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx @@ -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; diff --git a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx index 6df5d74ed40..8c4e43936a3 100644 --- a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx @@ -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 }, }, }) => , - size: '80px', + size: '95px', }, { id: 'labels', diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx index 4708744c602..16042f21bb9 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx @@ -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' }, ]} /> diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx index fc6d861e921..d7e6839d810 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx @@ -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 && ( - Every {every} + + Every {{ every }} + )} @@ -100,6 +103,11 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => {pendingPeriod} )} + {keepFiringFor && ( + + {keepFiringFor} + + )} {lastEvaluation && !isNullDate(lastEvaluation) && ( content={`${dateTimeFormat(lastEvaluation, { format: 'YYYY-MM-DD HH:mm:ss' })}`} theme="info" > - {`${dateTime(lastEvaluation).locale('en').fromNow(true)} ago`} + + {t('alerting.rule-details.last-evaluation-ago', '{{time}} ago', { + time: dateTime(lastEvaluation).locale('en').fromNow(true), + })} + )} diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx index 3445374554a..f7d741a92ef 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.test.tsx @@ -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(); @@ -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, }; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx index c45c3cdee34..f5b3b2d543d 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx @@ -93,6 +93,7 @@ export function RuleDetailsMatchingInstances(props: Props) { instanceTotals.alerting, instanceTotals.inactive, instanceTotals.pending, + instanceTotals.recovering, instanceTotals.nodata, ]); const hiddenInstancesCount = totalInstancesCount - visibleInstances.length; diff --git a/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx b/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx index 3295e2950e6..b3e5536bfff 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListStateView.tsx @@ -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.Firing]: 'Firing', [PromAlertingRuleState.Pending]: 'Pending', [PromAlertingRuleState.Inactive]: 'Normal', + [PromAlertingRuleState.Recovering]: 'Recovering', }; const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: CombinedRule[] }) => { diff --git a/public/app/features/alerting/unified/components/rules/RuleStats.tsx b/public/app/features/alerting/unified/components/rules/RuleStats.tsx index aa219753fba..e6cf4d66842 100644 --- a/public/app/features/alerting/unified/components/rules/RuleStats.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleStats.tsx @@ -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 = { 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(); + statsComponents.push( + + ); } if (stats.error) { - statsComponents.push(); + statsComponents.push( + + ); } if (stats.nodata) { - statsComponents.push(); + statsComponents.push( + + ); } if (stats[AlertInstanceTotalState.Pending]) { + const pendingStats = stats[AlertInstanceTotalState.Pending]; statsComponents.push( - + + ); + } + + if (stats[AlertInstanceTotalState.Recovering]) { + const recoveringStats = stats[AlertInstanceTotalState.Recovering]; + statsComponents.push( + ); } if (stats[AlertInstanceTotalState.Normal] && stats.paused) { + const normalStats = stats[AlertInstanceTotalState.Normal]; + const pausedStats = stats.paused; statsComponents.push( ); } if (stats[AlertInstanceTotalState.Normal] && !stats.paused) { + const normalStats = stats[AlertInstanceTotalState.Normal]; statsComponents.push( - + ); } if (stats.recording) { - statsComponents.push(); + const recordingStats = stats.recording; + statsComponents.push( + + ); } return statsComponents; diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx index 6bbf0c458d7..68ea773209d 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx @@ -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({ diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx index bb63b397a24..865cd7d11b0 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx +++ b/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx @@ -349,6 +349,12 @@ export function EventState({ state, showLabel = false, addFilter, type }: EventS tooltipContent: Boolean(reason) ? `Pending (${reason})` : 'Pending', labelText: Pending, }, + Recovering: { + iconName: 'circle', + iconColor: styles.warningColor, + tooltipContent: Boolean(reason) ? `Recovering (${reason})` : 'Recovering', + labelText: Recovering, + }, }; function onStateClick() { addFilter('state', baseState, type === 'from' ? 'stateFrom' : 'stateTo'); diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts index d78688c1ab8..a629b03dfb2 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts +++ b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts @@ -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, }, diff --git a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx index ee9d74854d7..64455d039ff 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx @@ -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 }, diff --git a/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx b/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx index 2dc9a5abf94..90d989fdcfc 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx @@ -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, }, diff --git a/public/app/features/alerting/unified/home/Insights.tsx b/public/app/features/alerting/unified/home/Insights.tsx index 14bd54b0682..22c283c10bf 100644 --- a/public/app/features/alerting/unified/home/Insights.tsx +++ b/public/app/features/alerting/unified/home/Insights.tsx @@ -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', diff --git a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts index e4278cb9852..a5ee387bca0 100644 --- a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts +++ b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts @@ -320,6 +320,7 @@ export function calculateRuleTotals(rule: Pick) nodata: countsByHealth.nodata, inactive: countsByState[PromAlertingRuleState.Inactive], pending: countsByState[PromAlertingRuleState.Pending], + recovering: countsByState[PromAlertingRuleState.Recovering], recording: recordingCount, }; } diff --git a/public/app/features/alerting/unified/insights/grafana/AlertsByStateScene.tsx b/public/app/features/alerting/unified/insights/grafana/AlertsByStateScene.tsx index f0cdd38654c..aa0ab6edd50 100644 --- a/public/app/features/alerting/unified/insights/grafana/AlertsByStateScene.tsx +++ b/public/app/features/alerting/unified/insights/grafana/AlertsByStateScene.tsx @@ -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') diff --git a/public/app/features/alerting/unified/insights/grafana/InstanceStatusScene.tsx b/public/app/features/alerting/unified/insights/grafana/InstanceStatusScene.tsx index 990d3d621e5..af933ce0c0d 100644 --- a/public/app/features/alerting/unified/insights/grafana/InstanceStatusScene.tsx +++ b/public/app/features/alerting/unified/insights/grafana/InstanceStatusScene.tsx @@ -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}"})` diff --git a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap index 692484862f3..16965687511 100644 --- a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap +++ b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap @@ -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", diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.ts index 7c6a0c824c0..0dbe86bca2a 100644 --- a/public/app/features/alerting/unified/rule-editor/formDefaults.ts +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.ts @@ -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: {}, diff --git a/public/app/features/alerting/unified/rule-list/StateView.tsx b/public/app/features/alerting/unified/rule-list/StateView.tsx index 4f47ae59ac5..1fd4efff11b 100644 --- a/public/app/features/alerting/unified/rule-list/StateView.tsx +++ b/public/app/features/alerting/unified/rule-list/StateView.tsx @@ -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.Firing]: 'Firing', [PromAlertingRuleState.Pending]: 'Pending', [PromAlertingRuleState.Inactive]: 'Normal', + [PromAlertingRuleState.Recovering]: 'Recovering', }; const RulesByState = ({ state, rules }: { state: PromAlertingRuleState; rules: CombinedRule[] }) => { diff --git a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx index 7316f43cd0f..4dc1cba7338 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx @@ -27,12 +27,14 @@ export enum RuleOperation { const icons: Record = { [PromAlertingRuleState.Inactive]: 'check-circle', [PromAlertingRuleState.Pending]: 'circle', + [PromAlertingRuleState.Recovering]: 'exclamation-circle', [PromAlertingRuleState.Firing]: 'exclamation-circle', }; const color: Record = { [PromAlertingRuleState.Inactive]: 'success', [PromAlertingRuleState.Pending]: 'warning', + [PromAlertingRuleState.Recovering]: 'warning', [PromAlertingRuleState.Firing]: 'error', }; @@ -40,6 +42,7 @@ const stateNames: Record = { [PromAlertingRuleState.Inactive]: 'Normal', [PromAlertingRuleState.Pending]: 'Pending', [PromAlertingRuleState.Firing]: 'Firing', + [PromAlertingRuleState.Recovering]: 'Recovering', }; const operationIcons: Record = { diff --git a/public/app/features/alerting/unified/types/rule-form.ts b/public/app/features/alerting/unified/types/rule-form.ts index a4cc2dc2da7..b7d02e431b4 100644 --- a/public/app/features/alerting/unified/types/rule-form.ts +++ b/public/app/features/alerting/unified/types/rule-form.ts @@ -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; diff --git a/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap b/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap index d3d728fd59f..4fad134846e 100644 --- a/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap +++ b/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap @@ -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": {}, } `; diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index 987629f185c..033a93c1272 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -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, diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index d89c976bf39..94fef4b7740 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -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, diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index b6bccd3f989..c0eb608e2d1 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -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 diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 680942f86f2..db994772bd7 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -304,7 +304,8 @@ function filterRules(props: PanelProps, 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) ); }); diff --git a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx index a77e61d44af..d2b1a7622ba 100644 --- a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx @@ -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, diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index e919102f8ce..2a7d462c2d7 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -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(UnifiedAlertListPanel).setPanelOptions((builder) => { builder @@ -168,6 +168,12 @@ const unifiedAlertList = new PanelPlugin(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', diff --git a/public/app/plugins/panel/alertlist/types.ts b/public/app/plugins/panel/alertlist/types.ts index 9e497041d45..b1f5ab868ec 100644 --- a/public/app/plugins/panel/alertlist/types.ts +++ b/public/app/plugins/panel/alertlist/types.ts @@ -20,6 +20,7 @@ export interface StateFilter { firing: boolean; pending: boolean; inactive?: boolean; // backwards compat + recovering: boolean; noData: boolean; normal: boolean; error: boolean; diff --git a/public/app/plugins/panel/alertlist/util.test.tsx b/public/app/plugins/panel/alertlist/util.test.tsx index b8513cb55b4..1956309b37e 100644 --- a/public/app/plugins/panel/alertlist/util.test.tsx +++ b/public/app/plugins/panel/alertlist/util.test.tsx @@ -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); diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts index a7984904696..af483eaa043 100644 --- a/public/app/plugins/panel/alertlist/util.ts +++ b/public/app/plugins/panel/alertlist/util.ts @@ -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)) || diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index de7a68ac1aa..ca40add000d 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -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 { grafana_alert: T; for?: string; + keep_firing_for?: string; annotations: Annotations; labels: Labels; } diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts index 2e5919efe8f..176a6b440c1 100644 --- a/public/app/types/unified-alerting.ts +++ b/public/app/types/unified-alerting.ts @@ -143,6 +143,7 @@ export interface CombinedRule { export enum AlertInstanceTotalState { Alerting = 'alerting', Pending = 'pending', + Recovering = 'recovering', Normal = 'inactive', NoData = 'nodata', Error = 'error', diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 80cb6e40757..8541acdc40e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": {