diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/budget_consumed.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/budget_consumed.tsx new file mode 100644 index 000000000000..85806a8b8d4f --- /dev/null +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/budget_consumed.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { ChangeEvent, useState } from 'react'; +import numeral from '@elastic/numeral'; + +interface Props { + initialBurnRate?: number; + errors?: string[]; + onChange: (burnRate: number) => void; + longLookbackWindowInHours: number; + sloTimeWindowInHours: number; +} + +export function BudgetConsumed({ + onChange, + initialBurnRate = 1, + longLookbackWindowInHours, + sloTimeWindowInHours, + errors, +}: Props) { + const [budgetConsumed, setBudgetConsumed] = useState( + ((initialBurnRate * longLookbackWindowInHours) / sloTimeWindowInHours) * 100 + ); + const hasError = errors !== undefined && errors.length > 0; + + const onBudgetConsumedChanged = (event: ChangeEvent) => { + const value = Number(event.target.value); + setBudgetConsumed(value); + const burnRate = sloTimeWindowInHours * (value / 100 / longLookbackWindowInHours); + onChange(burnRate); + }; + + return ( + + {i18n.translate('xpack.observability.slo.rules.budgetConsumed.rowLabel', { + defaultMessage: '% Budget consumed', + })}{' '} + + + } + fullWidth + isInvalid={hasError} + > + onBudgetConsumedChanged(event)} + data-test-subj="budgetConsumed" + /> + + ); +} diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx index 1dd225191cb8..ac7b92950fc9 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx @@ -8,13 +8,13 @@ import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ChangeEvent, useState } from 'react'; +import numeral from '@elastic/numeral'; interface Props { initialBurnRate?: number; maxBurnRate: number; errors?: string[]; onChange: (burnRate: number) => void; - helpText?: string; } export function BurnRate({ onChange, initialBurnRate = 1, maxBurnRate, errors }: Props) { @@ -48,10 +48,10 @@ export function BurnRate({ onChange, initialBurnRate = 1, maxBurnRate, errors }: > onBurnRateChange(event)} data-test-subj="burnRate" /> 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 fc3e0bf71452..e1e858df495a 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 @@ -118,21 +118,14 @@ export function BurnRateRuleEditor(props: Props) { )} - -
- {i18n.translate('xpack.observability.burnRateRuleEditor.h5.defineMultipleBurnRateLabel', { - defaultMessage: 'Define multiple burn rate windows', - })} -
-
- - - + {selectedSlo && ( + + )} ); } diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.test.ts b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.test.ts index d351a2555f6b..4e4d7fbc6655 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.test.ts +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.test.ts @@ -44,7 +44,7 @@ describe('ValidateBurnRateRule', () => { expect(errors.windows[0].burnRateThreshold).toHaveLength(1); }); - it('validates burnRateThreshold is between 1 and maxBurnRateThreshold', () => { + it('validates burnRateThreshold is between 0.01 and maxBurnRateThreshold', () => { expect( validateBurnRateRule( createTestParams({ @@ -55,7 +55,7 @@ describe('ValidateBurnRateRule', () => { ).toHaveLength(1); expect( - validateBurnRateRule(createTestParams({ burnRateThreshold: 0.99 })).errors.windows[0] + validateBurnRateRule(createTestParams({ burnRateThreshold: 0.001 })).errors.windows[0] .burnRateThreshold ).toHaveLength(1); diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.ts b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.ts index 20920350f592..c184a8fb5c44 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.ts +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/validation.ts @@ -46,7 +46,7 @@ export function validateBurnRateRule( const result = { longWindow: new Array(), burnRateThreshold: new Array() }; if (burnRateThreshold === undefined || maxBurnRateThreshold === undefined) { result.burnRateThreshold.push(BURN_RATE_THRESHOLD_REQUIRED); - } else if (sloId && (burnRateThreshold < 1 || burnRateThreshold > maxBurnRateThreshold)) { + } else if (sloId && (burnRateThreshold < 0.01 || burnRateThreshold > maxBurnRateThreshold)) { result.burnRateThreshold.push(getInvalidThresholdValueError(maxBurnRateThreshold)); } if (longWindow === undefined) { @@ -89,6 +89,6 @@ const BURN_RATE_THRESHOLD_REQUIRED = i18n.translate( const getInvalidThresholdValueError = (maxBurnRate: number) => i18n.translate('xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue', { - defaultMessage: 'Burn rate threshold must be between 1 and {maxBurnRate}.', + defaultMessage: 'Burn rate threshold must be between 0.01 and {maxBurnRate}.', values: { maxBurnRate }, }); 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 3f468e21c647..d4eb6e4627c2 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 @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -14,6 +14,8 @@ import { EuiSelect, EuiFormRow, EuiText, + EuiTitle, + EuiSwitch, } from '@elastic/eui'; import { SLOResponse } from '@kbn/slo-schema'; import { i18n } from '@kbn/i18n'; @@ -31,6 +33,7 @@ import { MEDIUM_PRIORITY_ACTION, } from '../../../common/constants'; import { WindowResult } from './validation'; +import { BudgetConsumed } from './budget_consumed'; interface WindowProps extends WindowSchema { slo?: SLOResponse; @@ -38,6 +41,7 @@ interface WindowProps extends WindowSchema { onDelete: (id: string) => void; disableDelete: boolean; errors: WindowResult; + budgetMode: boolean; } const ACTION_GROUP_OPTIONS = [ @@ -65,6 +69,7 @@ function Window({ onDelete, errors, disableDelete, + budgetMode = false, }: WindowProps) { const onLongWindowDurationChange = (duration: Duration) => { const longWindowDurationInMinutes = toMinutes(duration); @@ -112,6 +117,17 @@ function Window({ return 'N/A'; }; + const sloTimeWindowInHours = Math.round( + toMinutes(toDuration(slo?.timeWindow.duration ?? '30d')) / 60 + ); + + const computeBudgetConsumed = () => { + if (slo && longWindow.value > 0 && burnRateThreshold > 0) { + return (burnRateThreshold * longWindow.value) / sloTimeWindowInHours; + } + return 0; + }; + const allErrors = [...errors.longWindow, ...errors.burnRateThreshold]; return ( @@ -125,15 +141,27 @@ function Window({ errors={errors.longWindow} /> - - - + {!budgetMode && ( + + + + )} + {budgetMode && ( + + + + )} )} -

{getErrorBudgetExhaustionText(computeErrorBudgetExhaustionInHours())}

+

+ {getErrorBudgetExhaustionText( + computeErrorBudgetExhaustionInHours(), + computeBudgetConsumed(), + burnRateThreshold, + budgetMode + )} +

); } -const getErrorBudgetExhaustionText = (formattedHours: string) => - i18n.translate('xpack.observability.slo.rules.errorBudgetExhaustion.text', { - defaultMessage: '{formatedHours} hours until error budget exhaustion.', - values: { - formatedHours: formattedHours, - }, - }); +const getErrorBudgetExhaustionText = ( + formattedHours: string, + budgetConsumed: number, + burnRateThreshold: number, + budgetMode = false +) => + budgetMode + ? i18n.translate('xpack.observability.slo.rules.errorBudgetExhaustion.budgetMode.text', { + defaultMessage: + '{formatedHours} hours until error budget exhaustion. The burn rate threshold is {burnRateThreshold}x.', + values: { + formatedHours: formattedHours, + burnRateThreshold: numeral(burnRateThreshold).format('0[.0]'), + }, + }) + : i18n.translate('xpack.observability.slo.rules.errorBudgetExhaustion.burnRateMode.text', { + defaultMessage: + '{formatedHours} hours until error budget exhaustion. {budgetConsumed} budget consumed before first alert.', + values: { + formatedHours: formattedHours, + budgetConsumed: numeral(budgetConsumed).format('0.00%'), + }, + }); export const createNewWindow = ( slo?: SLOResponse, @@ -217,6 +268,7 @@ interface WindowsProps { } export function Windows({ slo, windows, errors, onChange, totalNumberOfWindows }: WindowsProps) { + const [budgetMode, setBudgetMode] = useState(false); const handleWindowChange = (windowDef: WindowSchema) => { onChange(windows.map((def) => (windowDef.id === def.id ? windowDef : def))); }; @@ -229,8 +281,20 @@ export function Windows({ slo, windows, errors, onChange, totalNumberOfWindows } onChange([...windows, createNewWindow()]); }; + const handleModeChange = () => { + setBudgetMode((previous) => !previous); + }; + return ( <> + +
+ {i18n.translate('xpack.observability.burnRateRuleEditor.h5.defineMultipleBurnRateLabel', { + defaultMessage: 'Define multiple burn rate windows', + })} +
+
+ {windows.map((windowDef, index) => { const windowErrors = errors[index] || { longWindow: new Array(), @@ -245,25 +309,41 @@ export function Windows({ slo, windows, errors, onChange, totalNumberOfWindows } onChange={handleWindowChange} onDelete={handleWindowDelete} disableDelete={windows.length === 1} + budgetMode={budgetMode} /> ); })} - - - + + + + + + + + + + + ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e40475d4aae3..2ec1a295cc06 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29135,7 +29135,6 @@ "xpack.observability.slo.list.sortByType": "Trier par {type}", "xpack.observability.slo.partitionByBadge": "Partition par {partitionKey}", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "Le seuil du taux d'avancement doit être compris entre 1 et {maxBurnRate}.", - "xpack.observability.slo.rules.errorBudgetExhaustion.text": "{formatedHours} heures restantes avant l'épuisement du budget d'erreurs.", "xpack.observability.slo.rules.groupByMessage": "Le SLO que vous avez sélectionné a été créé avec une partition sur \"{groupByField}\". Cette règle surveille et génère une alerte pour chaque instance trouvée dans le champ de partition.", "xpack.observability.slo.rules.longWindowDuration.tooltip": "Période historique sur laquelle le taux d'avancement est calculé. Une période historique plus courte de {shortWindowDuration} minutes (1/12 de la période historique) sera utilisée pour une récupération plus rapide", "xpack.observability.slo.slo.activeAlertsBadge.label": "{count, plural, one {# alerte} many {# alertes} other {Alertes #}}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8f58cdabe778..626dabd7c69e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29134,7 +29134,6 @@ "xpack.observability.slo.list.sortByType": "{type}で並べ替え", "xpack.observability.slo.partitionByBadge": "{partitionKey}でパーティション", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "バーンレートしきい値は1以上{maxBurnRate}以下でなければなりません。", - "xpack.observability.slo.rules.errorBudgetExhaustion.text": "予算オーバーエラーまで{formatedHours}時間。", "xpack.observability.slo.rules.groupByMessage": "選択したSLOは\"{groupByField}\"にパーティションが作成されました。このルールは、パーティションフィールドで見つかったすべてのインスタンスを監視し、アラートを生成します。", "xpack.observability.slo.rules.longWindowDuration.tooltip": "バーンレートが計算されるルックバック期間。ルックバック期間を{shortWindowDuration}分(ルックバック期間の1/12)と短くすることで、より高速な復帰が可能になります", "xpack.observability.slo.slo.activeAlertsBadge.label": "{count, plural, other {#件のアラート}}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 62084ac55eed..6c9c7e2d37e7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -29132,7 +29132,6 @@ "xpack.observability.slo.list.sortByType": "按 {type} 排序", "xpack.observability.slo.partitionByBadge": "按 {partitionKey} 分区", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "消耗速度阈值必须介于 1 和 {maxBurnRate} 之间。", - "xpack.observability.slo.rules.errorBudgetExhaustion.text": "{formatedHours} 小时,直到错误预算耗尽。", "xpack.observability.slo.rules.groupByMessage": "已使用分区在“{groupByField}”上创建您选定的 SLO。此规则将监测在分区字段中发现的每个实例并为其生成告警。", "xpack.observability.slo.rules.longWindowDuration.tooltip": "在其间计算消耗速度的回顾期。将使用 {shortWindowDuration} 分钟的较短回顾期(1/12 的回顾期)以便更快恢复", "xpack.observability.slo.slo.activeAlertsBadge.label": "{count, plural, other {# 个告警}}",