From 8551799668322fca5e5a05e9df391c4f92634b35 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 23 Jan 2024 15:49:41 -0500 Subject: [PATCH] feat(slo): burn rate alert details page (#174548) --- .../src/hooks/use_alerts_history.ts | 4 +- .../alert_details_app_section.tsx | 84 +++++ .../alerts_history/alerts_history_panel.tsx | 207 ++++++++++++ .../error_rate/error_rate_panel.tsx | 184 +++++++++++ .../burn_rate/alert_details/utils/alert.ts | 28 ++ .../alert_details/utils/last_duration_i18n.ts | 64 ++++ .../slo/burn_rate}/burn_rate.test.tsx | 0 .../slo/burn_rate}/burn_rate.tsx | 0 .../slo/burn_rate}/burn_rates.tsx | 104 ++---- .../slo/error_rate_chart/error_rate_chart.tsx | 29 +- .../error_rate_chart/use_lens_definition.ts | 297 +++++++----------- .../public/pages/alerts/alerts.test.tsx | 1 - .../public/pages/rules/rules.test.tsx | 1 - .../slo_details/components/slo_details.tsx | 52 ++- .../register_observability_rule_types.ts | 3 + .../public/utils/is_alert_details_enabled.ts | 6 +- .../lib/rules/slo_burn_rate/executor.ts | 1 + .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 20 files changed, 788 insertions(+), 289 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx create mode 100644 x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/alerts_history/alerts_history_panel.tsx create mode 100644 x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx create mode 100644 x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/alert.ts create mode 100644 x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/last_duration_i18n.ts rename x-pack/plugins/observability/public/{pages/slo_details/components => components/slo/burn_rate}/burn_rate.test.tsx (100%) rename x-pack/plugins/observability/public/{pages/slo_details/components => components/slo/burn_rate}/burn_rate.tsx (100%) rename x-pack/plugins/observability/public/{pages/slo_details/components => components/slo/burn_rate}/burn_rates.tsx (57%) diff --git a/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts b/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts index fee104bf8b3582..cc555bfb10b271 100644 --- a/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts +++ b/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { type HttpSetup } from '@kbn/core/public'; import { ALERT_DURATION, ALERT_RULE_UUID, @@ -13,10 +15,8 @@ import { ALERT_TIME_RANGE, ValidFeatureId, } from '@kbn/rule-data-utils'; -import { type HttpSetup } from '@kbn/core/public'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; import { useQuery } from '@tanstack/react-query'; -import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; export interface Props { http: HttpSetup | undefined; diff --git a/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx new file mode 100644 index 00000000000000..44f1e60a06f72d --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx @@ -0,0 +1,84 @@ +/* + * 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 { EuiFlexGroup, EuiLink } from '@elastic/eui'; +import { Rule } from '@kbn/alerting-plugin/common'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect } from 'react'; +import { useFetchSloDetails } from '../../../../hooks/slo/use_fetch_slo_details'; +import { AlertSummaryField } from '../../../../pages/alert_details/components/alert_summary'; +import { TopAlert } from '../../../../typings/alerts'; +import { BurnRateRuleParams } from '../../../../typings/slo'; +import { useKibana } from '../../../../utils/kibana_react'; +import { AlertsHistoryPanel } from './components/alerts_history/alerts_history_panel'; +import { ErrorRatePanel } from './components/error_rate/error_rate_panel'; + +export type BurnRateRule = Rule; +export type BurnRateAlert = TopAlert; + +interface AppSectionProps { + alert: BurnRateAlert; + rule: BurnRateRule; + ruleLink: string; + setAlertSummaryFields: React.Dispatch>; +} + +// eslint-disable-next-line import/no-default-export +export default function AlertDetailsAppSection({ + alert, + rule, + ruleLink, + setAlertSummaryFields, +}: AppSectionProps) { + const { + services: { + http: { basePath }, + }, + } = useKibana(); + + const sloId = alert.fields['kibana.alert.rule.parameters']!.sloId as string; + const instanceId = alert.fields['kibana.alert.instance.id']!; + const { isLoading, data: slo } = useFetchSloDetails({ sloId, instanceId }); + + useEffect(() => { + setAlertSummaryFields([ + { + label: i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.summaryField.slo', + { + defaultMessage: 'Source SLO', + } + ), + value: ( + + {slo?.name ?? '-'} + + ), + }, + { + label: i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.summaryField.rule', + { + defaultMessage: 'Rule', + } + ), + value: ( + + {rule.name} + + ), + }, + ]); + }, [alert, rule, ruleLink, setAlertSummaryFields, basePath, slo]); + + return ( + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/alerts_history/alerts_history_panel.tsx b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/alerts_history/alerts_history_panel.tsx new file mode 100644 index 00000000000000..47320da6cce5aa --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/alerts_history/alerts_history_panel.tsx @@ -0,0 +1,207 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiLoadingChart, + EuiLoadingSpinner, + EuiPanel, + EuiStat, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useAlertsHistory } from '@kbn/observability-alert-details'; +import rison from '@kbn/rison'; +import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; +import { GetSLOResponse } from '@kbn/slo-schema'; +import moment from 'moment'; +import React from 'react'; +import { convertTo } from '../../../../../../../common/utils/formatters'; +import { WindowSchema } from '../../../../../../typings'; +import { useKibana } from '../../../../../../utils/kibana_react'; +import { ErrorRateChart } from '../../../../error_rate_chart'; +import { BurnRateAlert, BurnRateRule } from '../../alert_details_app_section'; +import { getActionGroupFromReason } from '../../utils/alert'; + +interface Props { + slo?: GetSLOResponse; + alert: BurnRateAlert; + rule: BurnRateRule; + isLoading: boolean; +} + +export function AlertsHistoryPanel({ rule, slo, alert, isLoading }: Props) { + const { + services: { http }, + } = useKibana(); + const { isLoading: isAlertsHistoryLoading, data } = useAlertsHistory({ + featureIds: ['slo'], + ruleId: rule.id, + dateRange: { + from: 'now-30d', + to: 'now', + }, + http, + }); + + const actionGroup = getActionGroupFromReason(alert.reason); + const actionGroupWindow = ( + (alert.fields[ALERT_RULE_PARAMETERS]?.windows ?? []) as WindowSchema[] + ).find((window: WindowSchema) => window.actionGroup === actionGroup); + const dataTimeRange = { + from: moment().subtract(30, 'day').toDate(), + to: new Date(), + }; + + function getAlertsLink() { + const kuery = `kibana.alert.rule.uuid:"${rule.id}"`; + return http.basePath.prepend(`/app/observability/alerts?_a=${rison.encode({ kuery })}`); + } + + if (isLoading) { + return ; + } + + if (!slo) { + return null; + } + + return ( + + + + + + +

+ {i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.title', + { defaultMessage: '{sloName} alerts history', values: { sloName: slo.name } } + )} +

+
+
+ + + + + + +
+ + + + + {i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.subtitle', + { + defaultMessage: 'Last 30 days', + } + )} + + + +
+ + + + + ) : data.totalTriggeredAlerts ? ( + data.totalTriggeredAlerts + ) : ( + '-' + ) + } + titleColor="danger" + titleSize="m" + textAlign="left" + isLoading={isLoading} + data-test-subj="alertsTriggeredStats" + reverse + description={ + + + {i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.triggeredAlertsStatsTitle', + { defaultMessage: 'Alerts triggered' } + )} + + + } + /> + + + + ) : data.avgTimeToRecoverUS ? ( + convertTo({ + unit: 'minutes', + microseconds: data.avgTimeToRecoverUS, + extended: true, + }).formatted + ) : ( + '-' + ) + } + titleColor="default" + titleSize="m" + textAlign="left" + isLoading={isLoading} + data-test-subj="avgTimeToRecoverStat" + reverse + description={ + + + {i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.avgTimeToRecoverStatsTitle', + { defaultMessage: 'Avg time to recover' } + )} + + + } + /> + + + + + + {isAlertsHistoryLoading ? ( + + ) : ( + a.doc_count > 0) + .map((a) => ({ + date: new Date(a.key_as_string!), + total: a.doc_count, + }))} + showErrorRateAsLine + /> + )} + + +
+
+ ); +} diff --git a/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx new file mode 100644 index 00000000000000..59725b64c2401d --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx @@ -0,0 +1,184 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiLoadingChart, + EuiPanel, + EuiStat, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + ALERT_EVALUATION_VALUE, + ALERT_RULE_PARAMETERS, + ALERT_TIME_RANGE, +} from '@kbn/rule-data-utils'; +import { GetSLOResponse } from '@kbn/slo-schema'; +import React from 'react'; +import { WindowSchema } from '../../../../../../typings'; +import { useKibana } from '../../../../../../utils/kibana_react'; +import { ErrorRateChart } from '../../../../error_rate_chart'; +import { TimeRange } from '../../../../error_rate_chart/use_lens_definition'; +import { BurnRateAlert } from '../../alert_details_app_section'; +import { getActionGroupFromReason } from '../../utils/alert'; +import { getLastDurationInUnit } from '../../utils/last_duration_i18n'; + +function getDataTimeRange( + timeRange: { gte: string; lte?: string }, + window: WindowSchema +): TimeRange { + const windowDurationInMs = window.longWindow.value * 60 * 60 * 1000; + return { + from: new Date(new Date(timeRange.gte).getTime() - windowDurationInMs), + to: timeRange.lte ? new Date(timeRange.lte) : new Date(), + }; +} + +function getAlertTimeRange(timeRange: { gte: string; lte?: string }): TimeRange { + return { + from: new Date(timeRange.gte), + to: timeRange.lte ? new Date(timeRange.lte) : new Date(), + }; +} + +interface Props { + alert: BurnRateAlert; + slo?: GetSLOResponse; + isLoading: boolean; +} + +export function ErrorRatePanel({ alert, slo, isLoading }: Props) { + const { + services: { http }, + } = useKibana(); + + const actionGroup = getActionGroupFromReason(alert.reason); + const actionGroupWindow = ( + (alert.fields[ALERT_RULE_PARAMETERS]?.windows ?? []) as WindowSchema[] + ).find((window: WindowSchema) => window.actionGroup === actionGroup); + + // @ts-ignore + const dataTimeRange = getDataTimeRange(alert.fields[ALERT_TIME_RANGE], actionGroupWindow); + // @ts-ignore + const alertTimeRange = getAlertTimeRange(alert.fields[ALERT_TIME_RANGE]); + const burnRate = alert.fields[ALERT_EVALUATION_VALUE]; + + if (isLoading) { + return ; + } + + if (!slo) { + return null; + } + + return ( + + + + + + +

+ {i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.burnRate.title', + { defaultMessage: '{sloName} burn rate', values: { sloName: slo.name } } + )} +

+
+
+ + + + + + +
+ + + {getLastDurationInUnit(dataTimeRange)} + + +
+ + + + + + + + {i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.burnRate.thresholdBreachedTitle', + { defaultMessage: 'Threshold breached' } + )} + + + + + + + + + {i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.burnRate.tresholdSubtitle', + { + defaultMessage: 'Alert when > {threshold}x', + values: { + threshold: numeral(actionGroupWindow!.burnRateThreshold).format( + '0.[00]' + ), + }, + } + )} + + + } + /> + + + + + + + + +
+
+ ); +} diff --git a/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/alert.ts b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/alert.ts new file mode 100644 index 00000000000000..3fc20222d352e6 --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/alert.ts @@ -0,0 +1,28 @@ +/* + * 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_ID, + HIGH_PRIORITY_ACTION_ID, + LOW_PRIORITY_ACTION_ID, + MEDIUM_PRIORITY_ACTION_ID, +} from '../../../../../../common/constants'; + +export function getActionGroupFromReason(reason: string): string { + const prefix = reason.split(':')[0]?.toLowerCase() ?? undefined; + switch (prefix) { + case 'critical': + return ALERT_ACTION_ID; + case 'high': + return HIGH_PRIORITY_ACTION_ID; + case 'medium': + return MEDIUM_PRIORITY_ACTION_ID; + case 'low': + default: + return LOW_PRIORITY_ACTION_ID; + } +} diff --git a/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/last_duration_i18n.ts b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/last_duration_i18n.ts new file mode 100644 index 00000000000000..daae6e78391292 --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/burn_rate/alert_details/utils/last_duration_i18n.ts @@ -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 { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { TimeRange } from '../../../error_rate_chart/use_lens_definition'; + +export function getLastDurationInUnit(timeRange: TimeRange): string { + const duration = moment.duration(moment(timeRange.to).diff(timeRange.from)); + const durationInSeconds = duration.asSeconds(); + + const oneMinute = 60; + if (durationInSeconds < oneMinute) { + return i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.lastDurationInSeconds', + { + defaultMessage: 'Last {duration} seconds', + values: { + duration: Math.trunc(durationInSeconds), + }, + } + ); + } + + const twoHours = 2 * 60 * 60; + if (durationInSeconds < twoHours) { + return i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.lastDurationInMinutes', + { + defaultMessage: 'Last {duration} minutes', + values: { + duration: Math.trunc(duration.asMinutes()), + }, + } + ); + } + + const twoDays = 2 * 24 * 60 * 60; + if (durationInSeconds < twoDays) { + return i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.lastDurationInHours', + { + defaultMessage: 'Last {duration} hours', + values: { + duration: Math.trunc(duration.asHours()), + }, + } + ); + } + + return i18n.translate( + 'xpack.observability.slo.burnRateRule.alertDetailsAppSection.lastDurationInDays', + { + defaultMessage: 'Last {duration} days', + values: { + duration: Math.trunc(duration.asDays()), + }, + } + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/burn_rate.test.tsx b/x-pack/plugins/observability/public/components/slo/burn_rate/burn_rate.test.tsx similarity index 100% rename from x-pack/plugins/observability/public/pages/slo_details/components/burn_rate.test.tsx rename to x-pack/plugins/observability/public/components/slo/burn_rate/burn_rate.test.tsx diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/burn_rate.tsx b/x-pack/plugins/observability/public/components/slo/burn_rate/burn_rate.tsx similarity index 100% rename from x-pack/plugins/observability/public/pages/slo_details/components/burn_rate.tsx rename to x-pack/plugins/observability/public/components/slo/burn_rate/burn_rate.tsx diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/burn_rates.tsx b/x-pack/plugins/observability/public/components/slo/burn_rate/burn_rates.tsx similarity index 57% rename from x-pack/plugins/observability/public/pages/slo_details/components/burn_rates.tsx rename to x-pack/plugins/observability/public/components/slo/burn_rate/burn_rates.tsx index 54ee0dd11cf769..22ffe8ecb0ce53 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/burn_rates.tsx +++ b/x-pack/plugins/observability/public/components/slo/burn_rate/burn_rates.tsx @@ -12,94 +12,54 @@ import { EuiFlexItem, EuiPanel, EuiTitle, - htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; -import React, { useEffect, useState } from 'react'; -import { ErrorRateChart } from '../../../components/slo/error_rate_chart'; +import React, { useState } from 'react'; import { useFetchSloBurnRates } from '../../../hooks/slo/use_fetch_slo_burn_rates'; +import { ErrorRateChart } from '../error_rate_chart'; import { BurnRate } from './burn_rate'; interface Props { slo: SLOWithSummaryResponse; isAutoRefreshing?: boolean; + burnRateOptions: BurnRateOption[]; } -const CRITICAL = 'CRITICAL'; -const HIGH = 'HIGH'; -const MEDIUM = 'MEDIUM'; -const LOW = 'LOW'; - -const WINDOWS = [ - { name: CRITICAL, duration: '1h' }, - { name: HIGH, duration: '6h' }, - { name: MEDIUM, duration: '24h' }, - { name: LOW, duration: '72h' }, -]; +export interface BurnRateOption { + id: string; + label: string; + windowName: string; + threshold: number; + duration: number; +} -const TIME_RANGE_OPTIONS = [ - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.observability.slo.burnRates.fromRange.1hLabel', { - defaultMessage: '1h', - }), - windowName: CRITICAL, - threshold: 14.4, - duration: 1, - }, - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.observability.slo.burnRates.fromRange.6hLabel', { - defaultMessage: '6h', - }), - windowName: HIGH, - threshold: 6, - duration: 6, - }, - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.observability.slo.burnRates.fromRange.24hLabel', { - defaultMessage: '24h', - }), - windowName: MEDIUM, - threshold: 3, - duration: 24, - }, - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.observability.slo.burnRates.fromRange.72hLabel', { - defaultMessage: '72h', - }), - windowName: LOW, - threshold: 1, - duration: 72, - }, -]; +function getWindowsFromOptions(opts: BurnRateOption[]): Array<{ name: string; duration: string }> { + return opts.map((opt) => ({ name: opt.windowName, duration: `${opt.duration}h` })); +} -export function BurnRates({ slo, isAutoRefreshing }: Props) { +export function BurnRates({ slo, isAutoRefreshing, burnRateOptions }: Props) { + const [burnRateOption, setBurnRateOption] = useState(burnRateOptions[0]); const { isLoading, data } = useFetchSloBurnRates({ slo, shouldRefetch: isAutoRefreshing, - windows: WINDOWS, + windows: getWindowsFromOptions(burnRateOptions), }); - const [timeRangeIdSelected, setTimeRangeIdSelected] = useState(TIME_RANGE_OPTIONS[0].id); - const [timeRange, setTimeRange] = useState(TIME_RANGE_OPTIONS[0]); - const onChange = (optionId: string) => { - setTimeRangeIdSelected(optionId); + const onBurnRateOptionChange = (optionId: string) => { + const selected = burnRateOptions.find((opt) => opt.id === optionId) ?? burnRateOptions[0]; + setBurnRateOption(selected); }; - useEffect(() => { - const selected = - TIME_RANGE_OPTIONS.find((opt) => opt.id === timeRangeIdSelected) ?? TIME_RANGE_OPTIONS[0]; - setTimeRange(selected); - }, [timeRangeIdSelected]); - - const fromRange = moment().subtract(timeRange.duration, 'hour').toDate(); - const threshold = timeRange.threshold; - const burnRate = data?.burnRates.find((br) => br.name === timeRange.windowName)?.burnRate; + const dataTimeRange = { + from: moment().subtract(burnRateOption.duration, 'hour').toDate(), + to: new Date(), + }; + const threshold = burnRateOption.threshold; + const burnRate = data?.burnRates.find( + (curr) => curr.name === burnRateOption.windowName + )?.burnRate; return ( @@ -111,7 +71,7 @@ export function BurnRates({ slo, isAutoRefreshing }: Props) {

{i18n.translate('xpack.observability.slo.burnRate.title', { defaultMessage: 'Burn rate', - })}{' '} + })}

@@ -140,9 +100,9 @@ export function BurnRates({ slo, isAutoRefreshing }: Props) { legend={i18n.translate('xpack.observability.slo.burnRate.timeRangeBtnLegend', { defaultMessage: 'Select the time range', })} - options={TIME_RANGE_OPTIONS} - idSelected={timeRangeIdSelected} - onChange={(id) => onChange(id)} + options={burnRateOptions.map((opt) => ({ id: opt.id, label: opt.label }))} + idSelected={burnRateOption.id} + onChange={onBurnRateOptionChange} buttonSize="compressed" /> @@ -152,7 +112,7 @@ export function BurnRates({ slo, isAutoRefreshing }: Props) { - + diff --git a/x-pack/plugins/observability/public/components/slo/error_rate_chart/error_rate_chart.tsx b/x-pack/plugins/observability/public/components/slo/error_rate_chart/error_rate_chart.tsx index 7b84d8c05675fc..f5b1dd0e331cfe 100644 --- a/x-pack/plugins/observability/public/components/slo/error_rate_chart/error_rate_chart.tsx +++ b/x-pack/plugins/observability/public/components/slo/error_rate_chart/error_rate_chart.tsx @@ -11,22 +11,39 @@ import moment from 'moment'; import React from 'react'; import { useKibana } from '../../../utils/kibana_react'; import { getDelayInSecondsFromSLO } from '../../../utils/slo/get_delay_in_seconds_from_slo'; -import { useLensDefinition } from './use_lens_definition'; +import { AlertAnnotation, TimeRange, useLensDefinition } from './use_lens_definition'; interface Props { slo: SLOResponse; - fromRange: Date; + dataTimeRange: TimeRange; + threshold: number; + alertTimeRange?: TimeRange; + showErrorRateAsLine?: boolean; + annotations?: AlertAnnotation[]; } -export function ErrorRateChart({ slo, fromRange }: Props) { +export function ErrorRateChart({ + slo, + dataTimeRange, + threshold, + alertTimeRange, + showErrorRateAsLine, + annotations, +}: Props) { const { lens: { EmbeddableComponent }, } = useKibana().services; - const lensDef = useLensDefinition(slo); + const lensDef = useLensDefinition( + slo, + threshold, + alertTimeRange, + annotations, + showErrorRateAsLine + ); const delayInSeconds = getDelayInSecondsFromSLO(slo); - const from = moment(fromRange).subtract(delayInSeconds, 'seconds').toISOString(); - const to = moment().subtract(delayInSeconds, 'seconds').toISOString(); + const from = moment(dataTimeRange.from).subtract(delayInSeconds, 'seconds').toISOString(); + const to = moment(dataTimeRange.to).subtract(delayInSeconds, 'seconds').toISOString(); return ( 0 + ? annotations.map((annotation) => ({ + layerId: uuidv4(), + layerType: 'annotations', + annotations: [ + { + type: 'manual', + id: uuidv4(), + label: i18n.translate( + 'xpack.observability.slo.errorRateChart.alertAnnotationLabel', + { defaultMessage: '{total} alert', values: { total: annotation.total } } + ), + key: { + type: 'point_in_time', + timestamp: moment(annotation.date).toISOString(), + }, + lineWidth: 2, + color: euiTheme.colors.danger, + icon: 'alert', + }, + ], + ignoreGlobalFilters: true, + persistanceType: 'byValue', + })) + : []), ], }, query: { @@ -203,7 +262,9 @@ export function useLensDefinition(slo: SLOResponse): TypedLensByValueInput['attr customLabel: true, }, '9f69a7b0-34b9-4b76-9ff7-26dc1a06ec14': { - label: 'Error rate', + label: i18n.translate('xpack.observability.slo.errorRateChart.errorRateLabel', { + defaultMessage: 'Error rate', + }), dataType: 'number', operationType: 'formula', isBucketed: false, @@ -291,7 +352,9 @@ export function useLensDefinition(slo: SLOResponse): TypedLensByValueInput['attr customLabel: true, }, '9f69a7b0-34b9-4b76-9ff7-26dc1a06ec14': { - label: 'Error rate', + label: i18n.translate('xpack.observability.slo.errorRateChart.errorRateLabel', { + defaultMessage: 'Error rate', + }), dataType: 'number', operationType: 'formula', isBucketed: false, @@ -326,7 +389,7 @@ export function useLensDefinition(slo: SLOResponse): TypedLensByValueInput['attr linkToLayers: [], columns: { '0a42b72b-cd5a-4d59-81ec-847d97c268e6X0': { - label: 'Part of 14.4x', + label: `Part of ${threshold}x`, dataType: 'number', operationType: 'math', isBucketed: false, @@ -347,186 +410,36 @@ export function useLensDefinition(slo: SLOResponse): TypedLensByValueInput['attr }, text: `1 - ${slo.objective.target}`, }, - 14.4, + threshold, ], location: { min: 0, max: 17, }, - text: `(1 - ${slo.objective.target}) * 14.4`, + text: `(1 - ${slo.objective.target}) * ${threshold}`, }, }, references: [], customLabel: true, }, '0a42b72b-cd5a-4d59-81ec-847d97c268e6': { - label: '14.4x', + label: `${numeral(threshold).format('0.[00]')}x`, dataType: 'number', operationType: 'formula', isBucketed: false, scale: 'ratio', params: { // @ts-ignore - formula: `(1 - ${slo.objective.target}) * 14.4`, + formula: `(1 - ${slo.objective.target}) * ${threshold}`, isFormulaBroken: false, }, references: ['0a42b72b-cd5a-4d59-81ec-847d97c268e6X0'], customLabel: true, }, - '76d3bcc9-7d45-4b08-b2b1-8d3866ca0762X0': { - label: 'Part of 6x', - dataType: 'number', - operationType: 'math', - isBucketed: false, - scale: 'ratio', - params: { - // @ts-ignore - tinymathAst: { - type: 'function', - name: 'multiply', - args: [ - { - type: 'function', - name: 'subtract', - args: [1, slo.objective.target], - location: { - min: 1, - max: 9, - }, - text: `1 - ${slo.objective.target}`, - }, - 6, - ], - location: { - min: 0, - max: 14, - }, - text: `(1 - ${slo.objective.target}) * 6`, - }, - }, - references: [], - customLabel: true, - }, - '76d3bcc9-7d45-4b08-b2b1-8d3866ca0762': { - label: '6x', - dataType: 'number', - operationType: 'formula', - isBucketed: false, - scale: 'ratio', - params: { - // @ts-ignore - formula: `(1 - ${slo.objective.target}) * 6`, - isFormulaBroken: false, - }, - references: ['76d3bcc9-7d45-4b08-b2b1-8d3866ca0762X0'], - customLabel: true, - }, - 'c531a6b1-70dd-4918-bdd0-a21535a7af05X0': { - label: 'Part of 3x', - dataType: 'number', - operationType: 'math', - isBucketed: false, - scale: 'ratio', - params: { - // @ts-ignore - tinymathAst: { - type: 'function', - name: 'multiply', - args: [ - { - type: 'function', - name: 'subtract', - args: [1, slo.objective.target], - location: { - min: 1, - max: 9, - }, - text: `1 - ${slo.objective.target}`, - }, - 3, - ], - location: { - min: 0, - max: 14, - }, - text: `(1 - ${slo.objective.target}) * 3`, - }, - }, - references: [], - customLabel: true, - }, - 'c531a6b1-70dd-4918-bdd0-a21535a7af05': { - label: '3x', - dataType: 'number', - operationType: 'formula', - isBucketed: false, - scale: 'ratio', - params: { - // @ts-ignore - formula: `(1 - ${slo.objective.target}) * 3`, - isFormulaBroken: false, - }, - references: ['c531a6b1-70dd-4918-bdd0-a21535a7af05X0'], - customLabel: true, - }, - '61f9e663-10eb-41f7-b584-1f0f95418489X0': { - label: 'Part of 1x', - dataType: 'number', - operationType: 'math', - isBucketed: false, - scale: 'ratio', - params: { - // @ts-ignore - tinymathAst: { - type: 'function', - name: 'multiply', - args: [ - { - type: 'function', - name: 'subtract', - args: [1, slo.objective.target], - location: { - min: 1, - max: 9, - }, - text: `1 - ${slo.objective.target}`, - }, - 1, - ], - location: { - min: 0, - max: 14, - }, - text: `(1 - ${slo.objective.target}) * 1`, - }, - }, - references: [], - customLabel: true, - }, - '61f9e663-10eb-41f7-b584-1f0f95418489': { - label: '1x', - dataType: 'number', - operationType: 'formula', - isBucketed: false, - scale: 'ratio', - params: { - // @ts-ignore - formula: `(1 - ${slo.objective.target}) * 1`, - isFormulaBroken: false, - }, - references: ['61f9e663-10eb-41f7-b584-1f0f95418489X0'], - customLabel: true, - }, }, columnOrder: [ '0a42b72b-cd5a-4d59-81ec-847d97c268e6', '0a42b72b-cd5a-4d59-81ec-847d97c268e6X0', - '76d3bcc9-7d45-4b08-b2b1-8d3866ca0762X0', - '76d3bcc9-7d45-4b08-b2b1-8d3866ca0762', - 'c531a6b1-70dd-4918-bdd0-a21535a7af05X0', - 'c531a6b1-70dd-4918-bdd0-a21535a7af05', - '61f9e663-10eb-41f7-b584-1f0f95418489X0', - '61f9e663-10eb-41f7-b584-1f0f95418489', ], sampling: 1, ignoreGlobalFilters: false, diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx index 8977e9ae3e4f0e..38d5ae3a05852c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx @@ -60,7 +60,6 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ } as unknown as AppMountParameters, config: { unsafe: { - slo: { enabled: false }, alertDetails: { apm: { enabled: false }, metrics: { enabled: false }, diff --git a/x-pack/plugins/observability/public/pages/rules/rules.test.tsx b/x-pack/plugins/observability/public/pages/rules/rules.test.tsx index 904578fe75a03e..a9c1076cae854d 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.test.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.test.tsx @@ -45,7 +45,6 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ } as unknown as AppMountParameters, config: { unsafe: { - slo: { enabled: false }, alertDetails: { apm: { enabled: false }, metrics: { enabled: false }, diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx index 3bb1ea99ad0cb5..122ecd0c07aace 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx @@ -12,6 +12,7 @@ import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab, + htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; @@ -21,7 +22,7 @@ import { useLocation } from 'react-router-dom'; import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary'; import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter'; -import { BurnRates } from './burn_rates'; +import { BurnRateOption, BurnRates } from '../../../components/slo/burn_rate/burn_rates'; import { ErrorBudgetChartPanel } from './error_budget_chart_panel'; import { EventsChartPanel } from './events_chart_panel'; import { Overview } from './overview/overview'; @@ -38,6 +39,49 @@ const OVERVIEW_TAB_ID = 'overview'; const ALERTS_TAB_ID = 'alerts'; const DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; +const BURN_RATE_OPTIONS: BurnRateOption[] = [ + { + id: htmlIdGenerator()(), + label: i18n.translate('xpack.observability.slo.burnRates.fromRange.label', { + defaultMessage: '{duration}h', + values: { duration: 1 }, + }), + windowName: 'CRITICAL', + threshold: 14.4, + duration: 1, + }, + { + id: htmlIdGenerator()(), + label: i18n.translate('xpack.observability.slo.burnRates.fromRange.label', { + defaultMessage: '{duration}h', + values: { duration: 6 }, + }), + windowName: 'HIGH', + threshold: 6, + duration: 6, + }, + { + id: htmlIdGenerator()(), + label: i18n.translate('xpack.observability.slo.burnRates.fromRange.label', { + defaultMessage: '{duration}h', + values: { duration: 24 }, + }), + windowName: 'MEDIUM', + threshold: 3, + duration: 24, + }, + { + id: htmlIdGenerator()(), + label: i18n.translate('xpack.observability.slo.burnRates.fromRange.label', { + defaultMessage: '{duration}h', + values: { duration: 72 }, + }), + windowName: 'LOW', + threshold: 1, + duration: 72, + }, +]; + type TabId = typeof OVERVIEW_TAB_ID | typeof ALERTS_TAB_ID; export function SloDetails({ slo, isAutoRefreshing }: Props) { @@ -96,7 +140,11 @@ export function SloDetails({ slo, isAutoRefreshing }: Props) { - + import('../components/slo/burn_rate/alert_details/alert_details_app_section') + ), priority: 100, }); diff --git a/x-pack/plugins/observability/public/utils/is_alert_details_enabled.ts b/x-pack/plugins/observability/public/utils/is_alert_details_enabled.ts index 2b628f26ee7948..7672a67166069d 100644 --- a/x-pack/plugins/observability/public/utils/is_alert_details_enabled.ts +++ b/x-pack/plugins/observability/public/utils/is_alert_details_enabled.ts @@ -9,7 +9,11 @@ import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import type { ConfigSchema } from '../plugin'; import type { TopAlert } from '../typings/alerts'; -const ALLOWED_RULE_TYPES = ['apm.transaction_duration', 'logs.alert.document.count']; +const ALLOWED_RULE_TYPES = [ + 'apm.transaction_duration', + 'logs.alert.document.count', + 'slo.rules.burnRate', +]; const isUnsafeAlertDetailsFlag = ( subject: string diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts index e1891e68af58b1..9e30f945dbd434 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts @@ -164,6 +164,7 @@ export const getRuleExecutor = ({ sloId: slo.id, sloName: slo.name, sloInstanceId: instanceId, + slo, }; alert.scheduleActions(windowDef.actionGroup, context); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index be3e8b9b14bad2..1ce4b365d2d81f 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29007,10 +29007,6 @@ "xpack.observability.slo.burnRate.technicalPreviewBadgeTitle": "Version d'évaluation technique", "xpack.observability.slo.burnRate.timeRangeBtnLegend": "Sélectionner la plage temporelle", "xpack.observability.slo.burnRate.title": "Taux d'avancement", - "xpack.observability.slo.burnRates.fromRange.1hLabel": "1 h", - "xpack.observability.slo.burnRates.fromRange.24hLabel": "24 h", - "xpack.observability.slo.burnRates.fromRange.6hLabel": "6 h", - "xpack.observability.slo.burnRates.fromRange.72hLabel": "72 h", "xpack.observability.slo.deleteConfirmationModal.cancelButtonLabel": "Annuler", "xpack.observability.slo.deleteConfirmationModal.deleteButtonLabel": "Supprimer", "xpack.observability.slo.deleteConfirmationModal.descriptionText": "Vous ne pouvez pas récupérer ce SLO après l'avoir supprimé.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a2b9dbfe95c875..8236fe9e82240b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29008,10 +29008,6 @@ "xpack.observability.slo.burnRate.technicalPreviewBadgeTitle": "テクニカルプレビュー", "xpack.observability.slo.burnRate.timeRangeBtnLegend": "時間範囲を選択", "xpack.observability.slo.burnRate.title": "バーンレート", - "xpack.observability.slo.burnRates.fromRange.1hLabel": "1h", - "xpack.observability.slo.burnRates.fromRange.24hLabel": "24h", - "xpack.observability.slo.burnRates.fromRange.6hLabel": "6h", - "xpack.observability.slo.burnRates.fromRange.72hLabel": "72h", "xpack.observability.slo.deleteConfirmationModal.cancelButtonLabel": "キャンセル", "xpack.observability.slo.deleteConfirmationModal.deleteButtonLabel": "削除", "xpack.observability.slo.deleteConfirmationModal.descriptionText": "このSLOを削除した後、復元することはできません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 54261e08bc1e84..2147e2fec9f959 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28992,10 +28992,6 @@ "xpack.observability.slo.burnRate.technicalPreviewBadgeTitle": "技术预览", "xpack.observability.slo.burnRate.timeRangeBtnLegend": "选择时间范围", "xpack.observability.slo.burnRate.title": "消耗速度", - "xpack.observability.slo.burnRates.fromRange.1hLabel": "1h", - "xpack.observability.slo.burnRates.fromRange.24hLabel": "24h", - "xpack.observability.slo.burnRates.fromRange.6hLabel": "6h", - "xpack.observability.slo.burnRates.fromRange.72hLabel": "72h", "xpack.observability.slo.deleteConfirmationModal.cancelButtonLabel": "取消", "xpack.observability.slo.deleteConfirmationModal.deleteButtonLabel": "删除", "xpack.observability.slo.deleteConfirmationModal.descriptionText": "此 SLO 删除后无法恢复。",