mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: allow any "evaluate for" value >=0 in the alert rule form (#35807)
This commit is contained in:
parent
b05b5d5e3b
commit
781ab833bd
@ -48,6 +48,11 @@ export function addDurationToDate(date: Date | number, duration: Duration): Date
|
||||
return add(date, duration);
|
||||
}
|
||||
|
||||
export function durationToMilliseconds(duration: Duration): number {
|
||||
const now = new Date();
|
||||
return addDurationToDate(now, duration).getTime() - now.getTime();
|
||||
}
|
||||
|
||||
export function isValidDate(dateString: string) {
|
||||
return !isNaN(Date.parse(dateString));
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { PageToolbar, Button, useStyles2, CustomScrollbar, Spinner } from '@graf
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { AlertTypeStep } from './AlertTypeStep';
|
||||
import { ConditionsStep } from './ConditionsStep';
|
||||
import { DetailsStep } from './DetailsStep';
|
||||
import { QueryStep } from './QueryStep';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
@ -21,6 +20,8 @@ import { Link } from 'react-router-dom';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { CloudConditionsStep } from './CloudConditionsStep';
|
||||
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
|
||||
|
||||
type Props = {
|
||||
existing?: RuleWithLocation;
|
||||
@ -120,7 +121,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
{showStep2 && (
|
||||
<>
|
||||
<QueryStep />
|
||||
<ConditionsStep />
|
||||
{type === RuleFormType.cloud ? <CloudConditionsStep /> : <GrafanaConditionsStep />}
|
||||
<DetailsStep />
|
||||
</>
|
||||
)}
|
||||
|
@ -0,0 +1,62 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Field, Input, InputControl, Select, useStyles } from '@grafana/ui';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { timeOptions } from '../../utils/time';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { PreviewRule } from './PreviewRule';
|
||||
|
||||
export const CloudConditionsStep: FC = () => {
|
||||
const styles = useStyles(getStyles);
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
|
||||
<div className={styles.flexRow}>
|
||||
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
|
||||
<Input
|
||||
{...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a positive integer.' } })}
|
||||
width={8}
|
||||
/>
|
||||
</Field>
|
||||
<InputControl
|
||||
name="forTimeUnit"
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={timeOptions}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
width={15}
|
||||
className={styles.timeUnit}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<PreviewRule />
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
inlineField: css`
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
`,
|
||||
timeUnit: css`
|
||||
margin-left: ${theme.spacing.xs};
|
||||
`,
|
||||
});
|
@ -1,178 +0,0 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme, parseDuration, addDurationToDate } from '@grafana/data';
|
||||
import { Field, InlineLabel, Input, InputControl, Select, Switch, useStyles } from '@grafana/ui';
|
||||
import { useFormContext, RegisterOptions } from 'react-hook-form';
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { timeOptions, timeValidationPattern } from '../../utils/time';
|
||||
import { ConditionField } from './ConditionField';
|
||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { PreviewRule } from './PreviewRule';
|
||||
|
||||
const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
||||
|
||||
const timeRangeValidationOptions: RegisterOptions = {
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
},
|
||||
pattern: timeValidationPattern,
|
||||
validate: (value: string) => {
|
||||
const duration = parseDuration(value);
|
||||
if (Object.keys(duration).length) {
|
||||
const from = new Date();
|
||||
const to = addDurationToDate(from, duration);
|
||||
const diff = to.getTime() - from.getTime();
|
||||
if (diff < MIN_TIME_RANGE_STEP_S * 1000) {
|
||||
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
if (diff % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
|
||||
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const ConditionsStep: FC = () => {
|
||||
const styles = useStyles(getStyles);
|
||||
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const type = watch('type');
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||
{type === RuleFormType.grafana && (
|
||||
<>
|
||||
<ConditionField />
|
||||
<Field label="Evaluate">
|
||||
<div className={styles.flexRow}>
|
||||
<InlineLabel width={16} tooltip="How often the alert will be evaluated to see if it fires">
|
||||
Evaluate every
|
||||
</InlineLabel>
|
||||
<Field
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateEvery?.message}
|
||||
invalid={!!errors.evaluateEvery?.message}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input width={8} {...register('evaluateEvery', timeRangeValidationOptions)} />
|
||||
</Field>
|
||||
<InlineLabel
|
||||
width={7}
|
||||
tooltip='Once condition is breached, alert will go into pending state. If it is pending for longer than the "for" value, it will become a firing alert.'
|
||||
>
|
||||
for
|
||||
</InlineLabel>
|
||||
<Field
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateFor?.message}
|
||||
invalid={!!errors.evaluateFor?.message}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input width={8} {...register('evaluateFor', timeRangeValidationOptions)} />
|
||||
</Field>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Configure no data and error handling" horizontal={true} className={styles.switchField}>
|
||||
<Switch value={showErrorHandling} onChange={() => setShowErrorHandling(!showErrorHandling)} />
|
||||
</Field>
|
||||
{showErrorHandling && (
|
||||
<>
|
||||
<Field label="Alert state if no data or all values are null">
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<GrafanaAlertStatePicker
|
||||
{...field}
|
||||
width={42}
|
||||
includeNoData={true}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
/>
|
||||
)}
|
||||
name="noDataState"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Alert state if execution error or timeout">
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<GrafanaAlertStatePicker
|
||||
{...field}
|
||||
width={42}
|
||||
includeNoData={false}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
/>
|
||||
)}
|
||||
name="execErrState"
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{type === RuleFormType.cloud && (
|
||||
<>
|
||||
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
|
||||
<div className={styles.flexRow}>
|
||||
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
|
||||
<Input
|
||||
{...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a postive integer.' } })}
|
||||
width={8}
|
||||
/>
|
||||
</Field>
|
||||
<InputControl
|
||||
name="forTimeUnit"
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={timeOptions}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
width={15}
|
||||
className={styles.timeUnit}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<PreviewRule />
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
inlineField: css`
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
`,
|
||||
numberInput: css`
|
||||
width: 200px;
|
||||
& + & {
|
||||
margin-left: ${theme.spacing.sm};
|
||||
}
|
||||
`,
|
||||
timeUnit: css`
|
||||
margin-left: ${theme.spacing.xs};
|
||||
`,
|
||||
switchField: css`
|
||||
display: inline-flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: ${theme.spacing.md};
|
||||
& > div:first-child {
|
||||
margin-left: ${theme.spacing.sm};
|
||||
}
|
||||
`,
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
import { durationToMilliseconds, parseDuration } from '@grafana/data';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
|
||||
// a warning that will be shown if a problematic yet technically valid combination of "evaluate every" and "evaluate for" is enetered
|
||||
export const GrafanaConditionEvalWarning: FC = () => {
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
const evaluateFor = watch('evaluateFor');
|
||||
const evaluateEvery = watch('evaluateEvery');
|
||||
if (evaluateFor === '0') {
|
||||
return null;
|
||||
}
|
||||
const durationFor = parseDuration(evaluateFor);
|
||||
const durationEvery = parseDuration(evaluateEvery);
|
||||
if (isEmpty(durationFor) || isEmpty(durationEvery)) {
|
||||
return null;
|
||||
}
|
||||
const millisFor = durationToMilliseconds(durationFor);
|
||||
const millisEvery = durationToMilliseconds(durationEvery);
|
||||
if (millisFor && millisEvery && millisFor <= millisEvery) {
|
||||
return (
|
||||
<Alert severity="warning" title="">
|
||||
Setting a "for" duration that is less than or equal to the evaluation interval will result in the
|
||||
evaluation interval being used to calculate when an alert that has stopped receiving data will be closed.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
@ -0,0 +1,142 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme, parseDuration, durationToMilliseconds } from '@grafana/data';
|
||||
import { Field, InlineLabel, Input, InputControl, Switch, useStyles } from '@grafana/ui';
|
||||
import { useFormContext, RegisterOptions } from 'react-hook-form';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { positiveDurationValidationPattern, durationValidationPattern } from '../../utils/time';
|
||||
import { ConditionField } from './ConditionField';
|
||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { PreviewRule } from './PreviewRule';
|
||||
import { GrafanaConditionEvalWarning } from './GrafanaConditionEvalWarning';
|
||||
|
||||
const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
||||
|
||||
const forValidationOptions: RegisterOptions = {
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
},
|
||||
pattern: durationValidationPattern,
|
||||
};
|
||||
|
||||
const evaluateEveryValidationOptions: RegisterOptions = {
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
},
|
||||
pattern: positiveDurationValidationPattern,
|
||||
validate: (value: string) => {
|
||||
const duration = parseDuration(value);
|
||||
if (Object.keys(duration).length) {
|
||||
const diff = durationToMilliseconds(duration);
|
||||
if (diff < MIN_TIME_RANGE_STEP_S * 1000) {
|
||||
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
if (diff % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
|
||||
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const GrafanaConditionsStep: FC = () => {
|
||||
const styles = useStyles(getStyles);
|
||||
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||
<ConditionField />
|
||||
<Field label="Evaluate">
|
||||
<div className={styles.flexRow}>
|
||||
<InlineLabel width={16} tooltip="How often the alert will be evaluated to see if it fires">
|
||||
Evaluate every
|
||||
</InlineLabel>
|
||||
<Field
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateEvery?.message}
|
||||
invalid={!!errors.evaluateEvery?.message}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input width={8} {...register('evaluateEvery', evaluateEveryValidationOptions)} />
|
||||
</Field>
|
||||
<InlineLabel
|
||||
width={7}
|
||||
tooltip='Once condition is breached, alert will go into pending state. If it is pending for longer than the "for" value, it will become a firing alert.'
|
||||
>
|
||||
for
|
||||
</InlineLabel>
|
||||
<Field
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateFor?.message}
|
||||
invalid={!!errors.evaluateFor?.message}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input width={8} {...register('evaluateFor', forValidationOptions)} />
|
||||
</Field>
|
||||
</div>
|
||||
</Field>
|
||||
<GrafanaConditionEvalWarning />
|
||||
<Field label="Configure no data and error handling" horizontal={true} className={styles.switchField}>
|
||||
<Switch value={showErrorHandling} onChange={() => setShowErrorHandling(!showErrorHandling)} />
|
||||
</Field>
|
||||
{showErrorHandling && (
|
||||
<>
|
||||
<Field label="Alert state if no data or all values are null">
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<GrafanaAlertStatePicker
|
||||
{...field}
|
||||
width={42}
|
||||
includeNoData={true}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
/>
|
||||
)}
|
||||
name="noDataState"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Alert state if execution error or timeout">
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<GrafanaAlertStatePicker
|
||||
{...field}
|
||||
width={42}
|
||||
includeNoData={false}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
/>
|
||||
)}
|
||||
name="execErrState"
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<PreviewRule />
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
inlineField: css`
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
`,
|
||||
switchField: css`
|
||||
display: inline-flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: ${theme.spacing.md};
|
||||
& > div:first-child {
|
||||
margin-left: ${theme.spacing.sm};
|
||||
}
|
||||
`,
|
||||
});
|
@ -98,7 +98,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
||||
...defaultFormValues,
|
||||
name: ga.title,
|
||||
type: RuleFormType.grafana,
|
||||
evaluateFor: rule.for,
|
||||
evaluateFor: rule.for || '0',
|
||||
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
||||
noDataState: ga.no_data_state,
|
||||
execErrState: ga.exec_err_state,
|
||||
|
@ -19,9 +19,18 @@ export const timeOptions = Object.entries(TimeOptions).map(([key, value]) => ({
|
||||
value: value,
|
||||
}));
|
||||
|
||||
export const timeValidationPattern = {
|
||||
// 1h, 10m and such
|
||||
export const positiveDurationValidationPattern = {
|
||||
value: new RegExp(`^\\d+(${Object.values(TimeOptions).join('|')})$`),
|
||||
message: `Must be of format "(number)(unit)", for example "1m". Available units: ${Object.values(TimeOptions).join(
|
||||
message: `Must be of format "(number)(unit)" , for example "1m". Available units: ${Object.values(TimeOptions).join(
|
||||
', '
|
||||
)}`,
|
||||
};
|
||||
|
||||
// 1h, 10m or 0 (without units)
|
||||
export const durationValidationPattern = {
|
||||
value: new RegExp(`^\\d+(${Object.values(TimeOptions).join('|')})|0$`),
|
||||
message: `Must be of format "(number)(unit)", for example "1m", or just "0". Available units: ${Object.values(
|
||||
TimeOptions
|
||||
).join(', ')}`,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user