Alerting: Display last & next rule eval date plus eval duration (#64767)

* Display last & next rule eval date plus eval duration

* Show next evaluation date in a humanized format

Full date still visible on hover

* Only show next evaluation column is group has an interval
This commit is contained in:
Virginia Cepeda 2023-03-14 17:52:55 -03:00 committed by GitHub
parent 95aa9b374a
commit 6b95b3f8aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 102 additions and 11 deletions

View File

@ -1,12 +1,14 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, dateTime, dateTimeFormat } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2, Tooltip } from '@grafana/ui';
import { Time } from 'app/features/explore/Time';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { useCleanAnnotations } from '../../utils/annotations'; import { useCleanAnnotations } from '../../utils/annotations';
import { isRecordingRulerRule } from '../../utils/rules'; import { isRecordingRulerRule } from '../../utils/rules';
import { isNullDate } from '../../utils/time';
import { AlertLabels } from '../AlertLabels'; import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField'; import { DetailsField } from '../DetailsField';
@ -63,6 +65,8 @@ interface EvaluationBehaviorSummaryProps {
const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => { const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => {
let forDuration: string | undefined; let forDuration: string | undefined;
let every = rule.group.interval; let every = rule.group.interval;
let lastEvaluation = rule.promRule?.lastEvaluation;
let lastEvaluationDuration = rule.promRule?.evaluationTime;
// recording rules don't have a for duration // recording rules don't have a for duration
if (!isRecordingRulerRule(rule.rulerRule)) { if (!isRecordingRulerRule(rule.rulerRule)) {
@ -81,6 +85,26 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
{forDuration} {forDuration}
</DetailsField> </DetailsField>
)} )}
{lastEvaluation && !isNullDate(lastEvaluation) && (
<DetailsField label="Last evaluation" horizontal={true}>
<Tooltip
placement="top"
content={`${dateTimeFormat(lastEvaluation, { format: 'YYYY-MM-DD HH:mm:ss' })}`}
theme="info"
>
<span>{`${dateTime(lastEvaluation).locale('en').fromNow(true)} ago`}</span>
</Tooltip>
</DetailsField>
)}
{lastEvaluation && !isNullDate(lastEvaluation) && lastEvaluationDuration !== undefined && (
<DetailsField label="Evaluation time" horizontal={true}>
<Tooltip placement="top" content={`${lastEvaluationDuration}s`} theme="info">
<span>{Time({ timeInMs: lastEvaluationDuration * 1000, humanize: true })}</span>
</Tooltip>
</DetailsField>
)}
</> </>
); );
}; };

View File

@ -234,7 +234,13 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
)} )}
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<RulesTable showSummaryColumn={true} className={styles.rulesTable} showGuidelines={true} rules={group.rules} /> <RulesTable
showSummaryColumn={true}
className={styles.rulesTable}
showGuidelines={true}
showNextEvaluationColumn={Boolean(group.interval)}
rules={group.rules}
/>
)} )}
{isEditingGroup && ( {isEditingGroup && (
<EditCloudGroupModal <EditCloudGroupModal

View File

@ -1,14 +1,23 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import {
import { useStyles2 } from '@grafana/ui'; GrafanaTheme2,
addDurationToDate,
isValidDate,
isValidDuration,
parseDuration,
dateTimeFormat,
dateTime,
} from '@grafana/data';
import { useStyles2, Tooltip } from '@grafana/ui';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
import { useHasRuler } from '../../hooks/useHasRuler'; import { useHasRuler } from '../../hooks/useHasRuler';
import { Annotation } from '../../utils/constants'; import { Annotation } from '../../utils/constants';
import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules'; import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules';
import { isNullDate } from '../../utils/time';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines'; import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';
import { ProvisioningBadge } from '../Provisioning'; import { ProvisioningBadge } from '../Provisioning';
@ -29,6 +38,7 @@ interface Props {
showGuidelines?: boolean; showGuidelines?: boolean;
showGroupColumn?: boolean; showGroupColumn?: boolean;
showSummaryColumn?: boolean; showSummaryColumn?: boolean;
showNextEvaluationColumn?: boolean;
emptyMessage?: string; emptyMessage?: string;
className?: string; className?: string;
} }
@ -40,6 +50,7 @@ export const RulesTable = ({
emptyMessage = 'No rules found.', emptyMessage = 'No rules found.',
showGroupColumn = false, showGroupColumn = false,
showSummaryColumn = false, showSummaryColumn = false,
showNextEvaluationColumn = false,
}: Props) => { }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -54,7 +65,7 @@ export const RulesTable = ({
}); });
}, [rules]); }, [rules]);
const columns = useColumns(showSummaryColumn, showGroupColumn); const columns = useColumns(showSummaryColumn, showGroupColumn, showNextEvaluationColumn);
if (!rules.length) { if (!rules.length) {
return <div className={cx(wrapperClass, styles.emptyMessage)}>{emptyMessage}</div>; return <div className={cx(wrapperClass, styles.emptyMessage)}>{emptyMessage}</div>;
@ -101,9 +112,29 @@ export const getStyles = (theme: GrafanaTheme2) => ({
`, `,
}); });
function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) { function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNextEvaluationColumn: boolean) {
const { hasRuler, rulerRulesLoaded } = useHasRuler(); const { hasRuler, rulerRulesLoaded } = useHasRuler();
const calculateNextEvaluationDate = useCallback((rule: CombinedRule) => {
const isValidLastEvaluation =
rule.promRule?.lastEvaluation &&
!isNullDate(rule.promRule.lastEvaluation) &&
isValidDate(rule.promRule.lastEvaluation);
const isValidIntervalDuration = rule.group.interval && isValidDuration(rule.group.interval);
if (!isValidLastEvaluation || !isValidIntervalDuration) {
return;
}
const lastEvaluationDate = Date.parse(rule.promRule?.lastEvaluation || '');
const intervalDuration = parseDuration(rule.group.interval!);
const nextEvaluationDate = addDurationToDate(lastEvaluationDate, intervalDuration);
return {
humanized: dateTime(nextEvaluationDate).locale('en').fromNow(true),
fullDate: dateTimeFormat(nextEvaluationDate, { format: 'YYYY-MM-DD HH:mm:ss' }),
};
}, []);
return useMemo((): RuleTableColumnProps[] => { return useMemo((): RuleTableColumnProps[] => {
const columns: RuleTableColumnProps[] = [ const columns: RuleTableColumnProps[] = [
{ {
@ -128,7 +159,7 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
label: 'Name', label: 'Name',
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => rule.name, renderCell: ({ data: rule }) => rule.name,
size: 5, size: showNextEvaluationColumn ? 4 : 5,
}, },
{ {
id: 'provisioned', id: 'provisioned',
@ -169,9 +200,28 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
renderCell: ({ data: rule }) => { renderCell: ({ data: rule }) => {
return <Tokenize input={rule.annotations[Annotation.summary] ?? ''} />; return <Tokenize input={rule.annotations[Annotation.summary] ?? ''} />;
}, },
size: 5, size: showNextEvaluationColumn ? 4 : 5,
}); });
} }
if (showNextEvaluationColumn) {
columns.push({
id: 'nextEvaluation',
label: 'Next evaluation',
renderCell: ({ data: rule }) => {
const nextEvalInfo = calculateNextEvaluationDate(rule);
return (
nextEvalInfo?.fullDate && (
<Tooltip placement="top" content={`${nextEvalInfo?.fullDate}`} theme="info">
<span>in {nextEvalInfo?.humanized}</span>
</Tooltip>
)
);
},
size: 2,
});
}
if (showGroupColumn) { if (showGroupColumn) {
columns.push({ columns.push({
id: 'group', id: 'group',
@ -203,5 +253,12 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
}); });
return columns; return columns;
}, [showSummaryColumn, showGroupColumn, hasRuler, rulerRulesLoaded]); }, [
showSummaryColumn,
showGroupColumn,
showNextEvaluationColumn,
hasRuler,
rulerRulesLoaded,
calculateNextEvaluationDate,
]);
} }

View File

@ -99,3 +99,7 @@ export function parsePrometheusDuration(duration: string): number {
return totalDuration; return totalDuration;
} }
export const isNullDate = (date: string) => {
return date.includes('0001-01-01T00');
};