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:
co-authored by
Tom Ratcliffe
parent
09a7f9ba1c
commit
71c66acb2d
+1
-11
@@ -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,
|
||||
}),
|
||||
});
|
||||
+2
-2
@@ -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
-3
@@ -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);
|
||||
|
||||
+65
-8
@@ -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>
|
||||
);
|
||||
|
||||
+6
-1
@@ -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',
|
||||
|
||||
+5
@@ -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>
|
||||
)}
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
+3
-2
@@ -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({
|
||||
|
||||
+6
@@ -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');
|
||||
|
||||
+4
-1
@@ -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,
|
||||
},
|
||||
|
||||
+1
@@ -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 },
|
||||
|
||||
+3
@@ -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}"})`
|
||||
|
||||
+2
@@ -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)) ||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ export interface CombinedRule {
|
||||
export enum AlertInstanceTotalState {
|
||||
Alerting = 'alerting',
|
||||
Pending = 'pending',
|
||||
Recovering = 'recovering',
|
||||
Normal = 'inactive',
|
||||
NoData = 'nodata',
|
||||
Error = 'error',
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user