diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/alert_time_table.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/alert_time_table.tsx new file mode 100644 index 0000000000000..9a12f79b6c1b7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/alert_time_table.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBasicTable, EuiSpacer, EuiText, EuiTitle, HorizontalAlignment } from '@elastic/eui'; +import { SLOResponse } from '@kbn/slo-schema'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import numeral from '@elastic/numeral'; +import { WindowSchema } from '../../typings'; +import { toDuration, toMinutes } from '../../utils/slo/duration'; + +interface AlertTimeTableProps { + slo: SLOResponse; + windows: WindowSchema[]; +} + +const ERROR_RATES = [0.01, 0.1, 0.2, 0.5, 1]; + +function formatTime(minutes: number) { + if (minutes > 59) { + const mins = minutes % 60; + const hours = (minutes - mins) / 60; + return i18n.translate('xpack.observability.slo.rules.timeTable.minuteHoursLabel', { + defaultMessage: '{hours}h {mins}m', + values: { hours, mins }, + }); + } + return i18n.translate('xpack.observability.slo.rules.timeTable.minuteLabel', { + defaultMessage: '{minutes}m', + values: { minutes }, + }); +} + +export function AlertTimeTable({ windows, slo }: AlertTimeTableProps) { + const rows = ERROR_RATES.map((rate) => { + const windowTimes = windows.reduce((acc, windowDef, index) => { + const windowInMinutes = toMinutes( + toDuration(`${windowDef.longWindow.value}${windowDef.longWindow.unit}`) + ); + const timeInMinutes = Math.round( + ((1 - slo.objective.target) / rate) * windowInMinutes * windowDef.burnRateThreshold + ); + return { + ...acc, + [`column_${index + 1}`]: timeInMinutes < windowInMinutes ? timeInMinutes : null, + }; + }, {}); + return { rate, ...windowTimes }; + }) as Array<{ rate: number } & WindowSchema>; + + const columns = [ + { + field: 'rate', + name: i18n.translate('xpack.observability.slo.rules.timeTable.rateColumnLabel', { + defaultMessage: 'Error rate', + }), + render: (rate: number) => numeral(rate).format('0%'), + }, + ...windows.map((windowDef, index) => ({ + field: `column_${index + 1}`, + name: `${windowDef.longWindow.value}h @ ${numeral(windowDef.burnRateThreshold).format( + '0[.0]' + )}x`, + align: 'right' as HorizontalAlignment, + render: (time: number | null) => (time ? formatTime(time) : '-'), + })), + ]; + return ( + <> + +
+ {i18n.translate('xpack.observability.slo.rules.timeTable.title', { + defaultMessage: 'How long will it take for the alert to fire?', + })} +
+
+ + +

+ {i18n.translate('xpack.observability.slo.rules.timeTable.description', { + defaultMessage: + 'The table below lists the error rates and approximately how long it would take to receive your first alert with the current configuration.', + })} +

+
+ + + tableCaption={i18n.translate('xpack.observability.slo.rules.tableCaption', { + defaultMessage: 'Alerting time table', + })} + items={rows} + columns={columns} + /> + + + ); +} diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx index e1e858df495a3..8eb9ca972d28c 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx @@ -15,13 +15,15 @@ import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; import { BurnRateRuleParams, WindowSchema } from '../../typings'; import { SloSelector } from './slo_selector'; import { ValidationBurnRateRuleResult } from './validation'; -import { createNewWindow, Windows, calculateMaxBurnRateThreshold } from './windows'; +import { createNewWindow, Windows } from './windows'; import { ALERT_ACTION, HIGH_PRIORITY_ACTION, LOW_PRIORITY_ACTION, MEDIUM_PRIORITY_ACTION, } from '../../../common/constants'; +import { BURN_RATE_DEFAULTS } from './constants'; +import { AlertTimeTable } from './alert_time_table'; type Props = Pick< RuleTypeParamsExpressionProps, @@ -78,14 +80,12 @@ export function BurnRateRuleEditor(props: Props) { // When the SLO changes, recalculate the max burn rates useEffect(() => { - setWindowDefs((previous) => - previous.map((windowDef) => { - return { - ...windowDef, - maxBurnRateThreshold: calculateMaxBurnRateThreshold(windowDef.longWindow, selectedSlo), - }; - }) - ); + setWindowDefs(() => { + const burnRateDefaults = selectedSlo + ? BURN_RATE_DEFAULTS[selectedSlo?.timeWindow.duration] + : BURN_RATE_DEFAULTS['30d']; + return burnRateDefaults.map((partialWindow) => createNewWindow(selectedSlo, partialWindow)); + }); }, [selectedSlo]); useEffect(() => { @@ -119,12 +119,15 @@ export function BurnRateRuleEditor(props: Props) { )} {selectedSlo && ( - + <> + + + )} ); diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/constants.ts b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/constants.ts new file mode 100644 index 0000000000000..ceb4d5c370fd2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/constants.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ALERT_ACTION, + HIGH_PRIORITY_ACTION, + LOW_PRIORITY_ACTION, + MEDIUM_PRIORITY_ACTION, +} from '../../../common/constants'; + +import { WindowSchema } from '../../typings'; + +type PartialWindowSchema = Partial; + +const WEEKLY: PartialWindowSchema[] = [ + { + burnRateThreshold: 3.36, + longWindow: { value: 1, unit: 'h' }, + shortWindow: { value: 5, unit: 'm' }, + actionGroup: ALERT_ACTION.id, + }, + { + burnRateThreshold: 1.4, + longWindow: { value: 6, unit: 'h' }, + shortWindow: { value: 30, unit: 'm' }, + actionGroup: HIGH_PRIORITY_ACTION.id, + }, + { + burnRateThreshold: 0.7, + longWindow: { value: 24, unit: 'h' }, + shortWindow: { value: 120, unit: 'm' }, + actionGroup: MEDIUM_PRIORITY_ACTION.id, + }, + { + burnRateThreshold: 0.234, + longWindow: { value: 72, unit: 'h' }, + shortWindow: { value: 260, unit: 'm' }, + actionGroup: LOW_PRIORITY_ACTION.id, + }, +]; + +const MONTHLY: PartialWindowSchema[] = [ + { + burnRateThreshold: 14.4, + longWindow: { value: 1, unit: 'h' }, + shortWindow: { value: 5, unit: 'm' }, + actionGroup: ALERT_ACTION.id, + }, + { + burnRateThreshold: 6, + longWindow: { value: 6, unit: 'h' }, + shortWindow: { value: 30, unit: 'm' }, + actionGroup: HIGH_PRIORITY_ACTION.id, + }, + { + burnRateThreshold: 3, + longWindow: { value: 24, unit: 'h' }, + shortWindow: { value: 120, unit: 'm' }, + actionGroup: MEDIUM_PRIORITY_ACTION.id, + }, + { + burnRateThreshold: 1, + longWindow: { value: 72, unit: 'h' }, + shortWindow: { value: 260, unit: 'm' }, + actionGroup: LOW_PRIORITY_ACTION.id, + }, +]; + +const QUARTERLY: PartialWindowSchema[] = [ + { + burnRateThreshold: 43.2, + longWindow: { value: 1, unit: 'h' }, + shortWindow: { value: 5, unit: 'm' }, + actionGroup: ALERT_ACTION.id, + }, + { + burnRateThreshold: 18, + longWindow: { value: 6, unit: 'h' }, + shortWindow: { value: 30, unit: 'm' }, + actionGroup: HIGH_PRIORITY_ACTION.id, + }, + { + burnRateThreshold: 9, + longWindow: { value: 24, unit: 'h' }, + shortWindow: { value: 120, unit: 'm' }, + actionGroup: MEDIUM_PRIORITY_ACTION.id, + }, + { + burnRateThreshold: 3, + longWindow: { value: 72, unit: 'h' }, + shortWindow: { value: 260, unit: 'm' }, + actionGroup: LOW_PRIORITY_ACTION.id, + }, +]; + +export const BURN_RATE_DEFAULTS: Record = { + // Calendar Aligned + '1M': MONTHLY, + '1w': WEEKLY, + '90d': QUARTERLY, + '30d': MONTHLY, + '7d': WEEKLY, +}; diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/windows.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/windows.tsx index d4eb6e4627c29..125361940c1fe 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/windows.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/windows.tsx @@ -69,7 +69,7 @@ function Window({ onDelete, errors, disableDelete, - budgetMode = false, + budgetMode = true, }: WindowProps) { const onLongWindowDurationChange = (duration: Duration) => { const longWindowDurationInMinutes = toMinutes(duration); @@ -268,7 +268,7 @@ interface WindowsProps { } export function Windows({ slo, windows, errors, onChange, totalNumberOfWindows }: WindowsProps) { - const [budgetMode, setBudgetMode] = useState(false); + const [budgetMode, setBudgetMode] = useState(true); const handleWindowChange = (windowDef: WindowSchema) => { onChange(windows.map((def) => (windowDef.id === def.id ? windowDef : def))); }; @@ -336,9 +336,9 @@ export function Windows({ slo, windows, errors, onChange, totalNumberOfWindows }