Skip to content

Commit

Permalink
[SLO] Enable burn rate alert by default during creation via UI (elast…
Browse files Browse the repository at this point in the history
…ic#176317)

## Summary

This PR changes the SLO creation behavior via the UI. Instead of having
a checkbox to create the Burn Rate rule, with this PR, the Burn Rate
Rule will be created by default. The Burn Rate rule is only created by
default when using the UI, the create SLO API does not create a rule by
default.
  • Loading branch information
simianhacker authored and CoenWarmer committed Feb 15, 2024
1 parent 7d0a721 commit 0962b69
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ import { BurnRateRuleParams, WindowSchema } from '../../typings';
import { SloSelector } from './slo_selector';
import { ValidationBurnRateRuleResult } from './validation';
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';

Expand All @@ -38,54 +32,25 @@ export function BurnRateRuleEditor(props: Props) {
});

const [selectedSlo, setSelectedSlo] = useState<SLOResponse | undefined>(undefined);
const [windowDefs, setWindowDefs] = useState<WindowSchema[]>(ruleParams?.windows || []);

useEffect(() => {
setSelectedSlo(initialSlo);
setWindowDefs((previous) => {
if (previous.length > 0) {
return previous;
}
return createDefaultWindows(initialSlo);
});
}, [initialSlo]);

const onSelectedSlo = (slo: SLOResponse | undefined) => {
setSelectedSlo(slo);
setRuleParams('sloId', slo?.id);
};

const [windowDefs, setWindowDefs] = useState<WindowSchema[]>(
ruleParams?.windows || [
createNewWindow(selectedSlo, {
burnRateThreshold: 14.4,
longWindow: { value: 1, unit: 'h' },
shortWindow: { value: 5, unit: 'm' },
actionGroup: ALERT_ACTION.id,
}),
createNewWindow(selectedSlo, {
burnRateThreshold: 6,
longWindow: { value: 6, unit: 'h' },
shortWindow: { value: 30, unit: 'm' },
actionGroup: HIGH_PRIORITY_ACTION.id,
}),
createNewWindow(selectedSlo, {
burnRateThreshold: 3,
longWindow: { value: 24, unit: 'h' },
shortWindow: { value: 120, unit: 'm' },
actionGroup: MEDIUM_PRIORITY_ACTION.id,
}),
createNewWindow(selectedSlo, {
burnRateThreshold: 1,
longWindow: { value: 72, unit: 'h' },
shortWindow: { value: 360, unit: 'm' },
actionGroup: LOW_PRIORITY_ACTION.id,
}),
]
);

// When the SLO changes, recalculate the max burn rates
useEffect(() => {
setWindowDefs(() => {
const burnRateDefaults = selectedSlo
? BURN_RATE_DEFAULTS[selectedSlo?.timeWindow.duration]
: BURN_RATE_DEFAULTS['30d'];
return burnRateDefaults.map((partialWindow) => createNewWindow(selectedSlo, partialWindow));
return createDefaultWindows(slo);
});
}, [selectedSlo]);
setRuleParams('sloId', slo?.id);
};

useEffect(() => {
setRuleParams('windows', windowDefs);
Expand Down Expand Up @@ -131,3 +96,8 @@ export function BurnRateRuleEditor(props: Props) {
</>
);
}

function createDefaultWindows(slo: SLOResponse | undefined) {
const burnRateDefaults = slo ? BURN_RATE_DEFAULTS[slo.timeWindow.duration] : [];
return burnRateDefaults.map((partialWindow) => createNewWindow(slo, partialWindow));
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
EuiTitle,
EuiSwitch,
} from '@elastic/eui';
import { SLOResponse } from '@kbn/slo-schema';
import { CreateSLOInput, SLOResponse } from '@kbn/slo-schema';
import { i18n } from '@kbn/i18n';
import numeral from '@elastic/numeral';
import { v4 } from 'uuid';
Expand Down Expand Up @@ -51,7 +51,10 @@ const ACTION_GROUP_OPTIONS = [
{ value: LOW_PRIORITY_ACTION.id, text: LOW_PRIORITY_ACTION.name },
];

export const calculateMaxBurnRateThreshold = (longWindow: Duration, slo?: SLOResponse) => {
export const calculateMaxBurnRateThreshold = (
longWindow: Duration,
slo?: SLOResponse | CreateSLOInput
) => {
return slo
? Math.floor(toMinutes(toDuration(slo.timeWindow.duration)) / toMinutes(longWindow))
: Infinity;
Expand Down Expand Up @@ -244,7 +247,7 @@ const getErrorBudgetExhaustionText = (
});

export const createNewWindow = (
slo?: SLOResponse,
slo?: SLOResponse | CreateSLOInput,
partialWindow: Partial<WindowSchema> = {}
): WindowSchema => {
const longWindow = partialWindow.longWindow || { value: 1, unit: 'h' };
Expand Down
64 changes: 64 additions & 0 deletions x-pack/plugins/observability/public/hooks/use_create_rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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 { useMutation } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { BASE_ALERTING_API_PATH, RuleTypeParams } from '@kbn/alerting-plugin/common';
import { v4 } from 'uuid';
import {
CreateRuleRequestBody,
CreateRuleResponse,
} from '@kbn/alerting-plugin/common/routes/rule/apis/create';
import { useKibana } from '../utils/kibana_react';

export function useCreateRule<Params extends RuleTypeParams = never>() {
const {
http,
notifications: { toasts },
} = useKibana().services;

const createRule = useMutation<
CreateRuleResponse<Params>,
Error,
{ rule: CreateRuleRequestBody<Params> }
>(
['createRule'],
({ rule }) => {
try {
const ruleId = v4();
const body = JSON.stringify(rule);
return http.post(`${BASE_ALERTING_API_PATH}/rule/${ruleId}`, {
body,
});
} catch (e) {
throw new Error(`Unable to create rule: ${e}`);
}
},
{
onError: (_err) => {
toasts.addDanger(
i18n.translate('xpack.observability.rules.createRule.errorNotification.descriptionText', {
defaultMessage: 'Failed to create rule',
})
);
},

onSuccess: () => {
toasts.addSuccess(
i18n.translate(
'xpack.observability.rules.createRule.successNotification.descriptionText',
{
defaultMessage: 'Rule created',
}
)
);
},
}
);

return createRule;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,15 @@
* 2.0.
*/

import {
EuiButton,
EuiButtonEmpty,
EuiCheckbox,
EuiFlexGroup,
EuiIconTip,
EuiSpacer,
EuiSteps,
} from '@elastic/eui';
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiSpacer, EuiSteps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { GetSLOResponse } from '@kbn/slo-schema';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { InspectSLOPortal } from './common/inspect_slo_portal';
import { EquivalentApiRequest } from './common/equivalent_api_request';
import { BurnRateRuleFlyout } from '../../slos/components/common/burn_rate_rule_flyout';
import { paths } from '../../../../common/locators/paths';
import { useCreateSlo } from '../../../hooks/slo/use_create_slo';
import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo';
import { useUpdateSlo } from '../../../hooks/slo/use_update_slo';
import { useKibana } from '../../../utils/kibana_react';
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../constants';
Expand All @@ -32,17 +22,16 @@ import {
transformSloResponseToCreateSloForm,
transformValuesToUpdateSLOInput,
} from '../helpers/process_slo_form_values';
import {
CREATE_RULE_SEARCH_PARAM,
useAddRuleFlyoutState,
} from '../hooks/use_add_rule_flyout_state';
import { useParseUrlState } from '../hooks/use_parse_url_state';
import { useSectionFormValidation } from '../hooks/use_section_form_validation';
import { useShowSections } from '../hooks/use_show_sections';
import { CreateSLOForm } from '../types';
import { SloEditFormDescriptionSection } from './slo_edit_form_description_section';
import { SloEditFormIndicatorSection } from './slo_edit_form_indicator_section';
import { SloEditFormObjectiveSection } from './slo_edit_form_objective_section';
import { useCreateRule } from '../../../hooks/use_create_rule';
import { createBurnRateRuleRequestBody } from '../helpers/create_burn_rate_rule_request_body';
import { BurnRateRuleParams } from '../../../typings';

export interface Props {
slo?: GetSLOResponse;
Expand All @@ -57,22 +46,9 @@ export function SloEditForm({ slo }: Props) {
} = useKibana().services;

const isEditMode = slo !== undefined;
const { data: rules, isInitialLoading } = useFetchRulesForSlo({
sloIds: slo?.id ? [slo.id] : undefined,
});

const sloFormValuesFromUrlState = useParseUrlState();
const sloFormValuesFromSloResponse = transformSloResponseToCreateSloForm(slo);

const isAddRuleFlyoutOpen = useAddRuleFlyoutState(isEditMode);
const [isCreateRuleCheckboxChecked, setIsCreateRuleCheckboxChecked] = useState(true);

useEffect(() => {
if (isEditMode && rules && rules[slo.id].length) {
setIsCreateRuleCheckboxChecked(false);
}
}, [isEditMode, rules, slo]);

const methods = useForm<CreateSLOForm>({
defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES,
values: sloFormValuesFromUrlState ? sloFormValuesFromUrlState : sloFormValuesFromSloResponse,
Expand All @@ -97,6 +73,8 @@ export function SloEditForm({ slo }: Props) {

const { mutateAsync: createSlo, isLoading: isCreateSloLoading } = useCreateSlo();
const { mutateAsync: updateSlo, isLoading: isUpdateSloLoading } = useUpdateSlo();
const { mutateAsync: createBurnRateRule, isLoading: isCreateBurnRateRuleLoading } =
useCreateRule<BurnRateRuleParams>();

const handleSubmit = async () => {
const isValid = await trigger();
Expand All @@ -108,30 +86,15 @@ export function SloEditForm({ slo }: Props) {

if (isEditMode) {
const processedValues = transformValuesToUpdateSLOInput(values);

if (isCreateRuleCheckboxChecked) {
await updateSlo({ sloId: slo.id, slo: processedValues });
navigate(
basePath.prepend(
`${paths.observability.sloEdit(slo.id)}?${CREATE_RULE_SEARCH_PARAM}=true`
)
);
} else {
updateSlo({ sloId: slo.id, slo: processedValues });
navigate(basePath.prepend(paths.observability.slos));
}
updateSlo({ sloId: slo.id, slo: processedValues });
navigate(basePath.prepend(paths.observability.slos));
} else {
const processedValues = transformCreateSLOFormToCreateSLOInput(values);

if (isCreateRuleCheckboxChecked) {
const { id } = await createSlo({ slo: processedValues });
navigate(
basePath.prepend(`${paths.observability.sloEdit(id)}?${CREATE_RULE_SEARCH_PARAM}=true`)
);
} else {
createSlo({ slo: processedValues });
navigate(basePath.prepend(paths.observability.slos));
}
const resp = await createSlo({ slo: processedValues });
await createBurnRateRule({
rule: createBurnRateRuleRequestBody({ ...processedValues, id: resp.id }),
});
navigate(basePath.prepend(paths.observability.slos));
}
};

Expand All @@ -140,10 +103,6 @@ export function SloEditForm({ slo }: Props) {
[navigateToUrl]
);

const handleChangeCheckbox = () => {
setIsCreateRuleCheckboxChecked(!isCreateRuleCheckboxChecked);
};

return (
<>
<FormProvider {...methods}>
Expand Down Expand Up @@ -175,44 +134,14 @@ export function SloEditForm({ slo }: Props) {
]}
/>

<EuiFlexGroup direction="row" gutterSize="s">
<EuiCheckbox
id="createNewRuleCheckbox"
checked={isCreateRuleCheckboxChecked}
disabled={isInitialLoading}
data-test-subj="createNewRuleCheckbox"
label={
<>
<span>
{i18n.translate('xpack.observability.slo.sloEdit.createAlert.title', {
defaultMessage: 'Create an',
})}{' '}
<strong>
{i18n.translate('xpack.observability.slo.sloEdit.createAlert.ruleName', {
defaultMessage: 'SLO burn rate alert rule',
})}
</strong>
</span>
<EuiIconTip
content={
'Selecting this will allow you to create a new alert rule for this SLO upon saving.'
}
position="top"
/>
</>
}
onChange={handleChangeCheckbox}
/>
</EuiFlexGroup>

<EuiSpacer size="m" />

<EuiFlexGroup direction="row" gutterSize="s">
<EuiButton
color="primary"
data-test-subj="sloFormSubmitButton"
fill
isLoading={isCreateSloLoading || isUpdateSloLoading}
isLoading={isCreateSloLoading || isUpdateSloLoading || isCreateBurnRateRuleLoading}
onClick={handleSubmit}
>
{isEditMode
Expand Down Expand Up @@ -243,12 +172,6 @@ export function SloEditForm({ slo }: Props) {
</EuiFlexGroup>
<InspectSLOPortal trigger={trigger} getValues={getValues} slo={slo} />
</FormProvider>

<BurnRateRuleFlyout
slo={slo as GetSLOResponse}
isAddRuleFlyoutOpen={isAddRuleFlyoutOpen}
canChangeTrigger={false}
/>
</>
);
}
Loading

0 comments on commit 0962b69

Please sign in to comment.