Skip to content

Commit

Permalink
[SLO] Set "budget consumed mode" as the default mode for burn rate ru…
Browse files Browse the repository at this point in the history
…le configuration (#171433)

## Summary

This PR sets the "budget consumed" mode as the default mode for
configuring the SLO Burn Rate Rule. This PR also adds a time table to
help the user understand when they can expect their SLO to fire based on
the burn rate windows and sample error rates.

<img width="549" alt="image"
src="https://github.com/elastic/kibana/assets/41702/01035b5d-60b9-40c8-9034-7ecd6a904bd5">

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
simianhacker and kibanamachine authored Dec 5, 2023
1 parent d922ae0 commit 426d8ac
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<>
<EuiTitle size="xs">
<h5>
{i18n.translate('xpack.observability.slo.rules.timeTable.title', {
defaultMessage: 'How long will it take for the alert to fire?',
})}
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<p>
{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.',
})}
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiBasicTable<{ rate: number } & WindowSchema>
tableCaption={i18n.translate('xpack.observability.slo.rules.tableCaption', {
defaultMessage: 'Alerting time table',
})}
items={rows}
columns={columns}
/>
<EuiSpacer size="l" />
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BurnRateRuleParams>,
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -119,12 +119,15 @@ export function BurnRateRuleEditor(props: Props) {
)}
<EuiSpacer size="l" />
{selectedSlo && (
<Windows
slo={selectedSlo}
windows={windowDefs}
onChange={setWindowDefs}
errors={errors.windows}
/>
<>
<Windows
slo={selectedSlo}
windows={windowDefs}
onChange={setWindowDefs}
errors={errors.windows}
/>
<AlertTimeTable slo={selectedSlo} windows={windowDefs} />
</>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WindowSchema>;

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<string, PartialWindowSchema[]> = {
// Calendar Aligned
'1M': MONTHLY,
'1w': WEEKLY,
'90d': QUARTERLY,
'30d': MONTHLY,
'7d': WEEKLY,
};
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function Window({
onDelete,
errors,
disableDelete,
budgetMode = false,
budgetMode = true,
}: WindowProps) {
const onLongWindowDurationChange = (duration: Duration) => {
const longWindowDurationInMinutes = toMinutes(duration);
Expand Down Expand Up @@ -268,7 +268,7 @@ interface WindowsProps {
}

export function Windows({ slo, windows, errors, onChange, totalNumberOfWindows }: WindowsProps) {
const [budgetMode, setBudgetMode] = useState<boolean>(false);
const [budgetMode, setBudgetMode] = useState<boolean>(true);
const handleWindowChange = (windowDef: WindowSchema) => {
onChange(windows.map((def) => (windowDef.id === def.id ? windowDef : def)));
};
Expand Down Expand Up @@ -336,9 +336,9 @@ export function Windows({ slo, windows, errors, onChange, totalNumberOfWindows }
<EuiSwitch
compressed
onChange={handleModeChange}
checked={budgetMode}
label={i18n.translate('xpack.observability.slo.rules.useBudgetConsumedModeLabel', {
defaultMessage: 'Budget consumed mode',
checked={!budgetMode}
label={i18n.translate('xpack.observability.slo.rules.useBurnRateModeLabel', {
defaultMessage: 'Burn rate mode',
})}
/>
</EuiFlexItem>
Expand Down

0 comments on commit 426d8ac

Please sign in to comment.