feat(slo): burn rate alert details page (#174548)

This commit is contained in:
Kevin Delemme 2024-01-23 15:49:41 -05:00 committed by GitHub
parent 68d1bac8b8
commit 3e9cc8d692
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 788 additions and 289 deletions

View file

@ -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;

View file

@ -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<BurnRateRuleParams>;
export type BurnRateAlert = TopAlert;
interface AppSectionProps {
alert: BurnRateAlert;
rule: BurnRateRule;
ruleLink: string;
setAlertSummaryFields: React.Dispatch<React.SetStateAction<AlertSummaryField[] | undefined>>;
}
// 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: (
<EuiLink data-test-subj="sloLink" href={basePath.prepend(alert.link!)}>
{slo?.name ?? '-'}
</EuiLink>
),
},
{
label: i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.summaryField.rule',
{
defaultMessage: 'Rule',
}
),
value: (
<EuiLink data-test-subj="ruleLink" href={ruleLink}>
{rule.name}
</EuiLink>
),
},
]);
}, [alert, rule, ruleLink, setAlertSummaryFields, basePath, slo]);
return (
<EuiFlexGroup direction="column" data-test-subj="overviewSection">
<ErrorRatePanel alert={alert} slo={slo} isLoading={isLoading} />
<AlertsHistoryPanel alert={alert} rule={rule} slo={slo} isLoading={isLoading} />
</EuiFlexGroup>
);
}

View file

@ -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 <EuiLoadingChart size="m" mono data-test-subj="loading" />;
}
if (!slo) {
return null;
}
return (
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="alertsHistoryPanel">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexGroup direction="row" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.title',
{ defaultMessage: '{sloName} alerts history', values: { sloName: slo.name } }
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink color="text" href={getAlertsLink()} data-test-subj="alertsLink">
<EuiIcon type="sortRight" style={{ marginRight: '4px' }} />
<FormattedMessage
id="xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.alertsLink"
defaultMessage="View alerts"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.subtitle',
{
defaultMessage: 'Last 30 days',
}
)}
</span>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="row" gutterSize="m" justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiStat
title={
isAlertsHistoryLoading ? (
<EuiLoadingSpinner size="s" />
) : data.totalTriggeredAlerts ? (
data.totalTriggeredAlerts
) : (
'-'
)
}
titleColor="danger"
titleSize="m"
textAlign="left"
isLoading={isLoading}
data-test-subj="alertsTriggeredStats"
reverse
description={
<EuiTextColor color="default">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.triggeredAlertsStatsTitle',
{ defaultMessage: 'Alerts triggered' }
)}
</span>
</EuiTextColor>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiStat
title={
isAlertsHistoryLoading ? (
<EuiLoadingSpinner size="s" />
) : 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={
<EuiTextColor color="default">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.avgTimeToRecoverStatsTitle',
{ defaultMessage: 'Avg time to recover' }
)}
</span>
</EuiTextColor>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="row" gutterSize="m" justifyContent="flexStart">
<EuiFlexItem>
{isAlertsHistoryLoading ? (
<EuiLoadingSpinner size="s" />
) : (
<ErrorRateChart
slo={slo}
dataTimeRange={dataTimeRange}
threshold={actionGroupWindow!.burnRateThreshold}
annotations={data.histogramTriggeredAlerts
.filter((a) => a.doc_count > 0)
.map((a) => ({
date: new Date(a.key_as_string!),
total: a.doc_count,
}))}
showErrorRateAsLine
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -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 <EuiLoadingChart size="m" mono data-test-subj="loading" />;
}
if (!slo) {
return null;
}
return (
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="burnRatePanel">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexGroup direction="row" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.burnRate.title',
{ defaultMessage: '{sloName} burn rate', values: { sloName: slo.name } }
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
color="text"
data-test-subj="o11yErrorRatePanelSloDetailsLink"
href={http.basePath.prepend(alert.link!)}
>
<EuiIcon type="sortRight" style={{ marginRight: '4px' }} />
<FormattedMessage
id="xpack.observability.slo.burnRateRule.alertDetailsAppSection.burnRate.sloDetailsLink"
defaultMessage="SLO details"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<span>{getLastDurationInUnit(dataTimeRange)}</span>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="row" gutterSize="m">
<EuiFlexItem grow={1}>
<EuiPanel color="danger" hasShadow={false} paddingSize="s" grow={false}>
<EuiFlexGroup
justifyContent="spaceBetween"
direction="column"
style={{ minHeight: '100%' }}
>
<EuiFlexItem>
<EuiText color="default" size="m">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.burnRate.thresholdBreachedTitle',
{ defaultMessage: 'Threshold breached' }
)}
<EuiIcon type="warning" style={{ marginLeft: '4px' }} />
</span>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiStat
title={`${numeral(burnRate).format('0.[00]')}x`}
titleColor="default"
titleSize="s"
textAlign="right"
isLoading={isLoading}
data-test-subj="burnRateStat"
description={
<EuiTextColor color="default">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.burnRate.tresholdSubtitle',
{
defaultMessage: 'Alert when > {threshold}x',
values: {
threshold: numeral(actionGroupWindow!.burnRateThreshold).format(
'0.[00]'
),
},
}
)}
</span>
</EuiTextColor>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={5}>
<ErrorRateChart
slo={slo}
dataTimeRange={dataTimeRange}
alertTimeRange={alertTimeRange}
threshold={actionGroupWindow!.burnRateThreshold}
showErrorRateAsLine
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -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;
}
}

View file

@ -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()),
},
}
);
}

View file

@ -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';
export interface BurnRateOption {
id: string;
label: string;
windowName: string;
threshold: number;
duration: number;
}
const WINDOWS = [
{ name: CRITICAL, duration: '1h' },
{ name: HIGH, duration: '6h' },
{ name: MEDIUM, duration: '24h' },
{ name: LOW, duration: '72h' },
];
function getWindowsFromOptions(opts: BurnRateOption[]): Array<{ name: string; duration: string }> {
return opts.map((opt) => ({ name: opt.windowName, duration: `${opt.duration}h` }));
}
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,
},
];
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 (
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="burnRatePanel">
@ -111,7 +71,7 @@ export function BurnRates({ slo, isAutoRefreshing }: Props) {
<h2>
{i18n.translate('xpack.observability.slo.burnRate.title', {
defaultMessage: 'Burn rate',
})}{' '}
})}
</h2>
</EuiTitle>
</EuiFlexItem>
@ -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"
/>
</EuiFlexItem>
@ -152,7 +112,7 @@ export function BurnRates({ slo, isAutoRefreshing }: Props) {
<BurnRate threshold={threshold} burnRate={burnRate} slo={slo} isLoading={isLoading} />
</EuiFlexItem>
<EuiFlexItem grow={3}>
<ErrorRateChart slo={slo} fromRange={fromRange} />
<ErrorRateChart slo={slo} dataTimeRange={dataTimeRange} threshold={threshold} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>

View file

@ -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 (
<EmbeddableComponent

View file

@ -5,17 +5,35 @@
* 2.0.
*/
import { useEuiTheme } from '@elastic/eui';
import { transparentize, useEuiTheme } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { ALL_VALUE, SLOResponse, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema';
import moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../common/slo/constants';
export function useLensDefinition(slo: SLOResponse): TypedLensByValueInput['attributes'] {
export interface TimeRange {
from: Date;
to: Date;
}
export interface AlertAnnotation {
date: Date;
total: number;
}
export function useLensDefinition(
slo: SLOResponse,
threshold: number,
alertTimeRange?: TimeRange,
annotations?: AlertAnnotation[],
showErrorRateAsLine?: boolean
): TypedLensByValueInput['attributes'] {
const { euiTheme } = useEuiTheme();
const interval = timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)
? slo.objective.timesliceWindow
: '60s';
const interval = 'auto';
return {
title: 'SLO Error Rate',
@ -58,26 +76,21 @@ export function useLensDefinition(slo: SLOResponse): TypedLensByValueInput['attr
layerId: '8730e8af-7dac-430e-9cef-3b9989ff0866',
accessors: ['9f69a7b0-34b9-4b76-9ff7-26dc1a06ec14'],
position: 'top',
seriesType: 'area',
seriesType: !!showErrorRateAsLine ? 'line' : 'area',
showGridlines: false,
layerType: 'data',
xAccessor: '627ded04-eae0-4437-83a1-bbb6138d2c3b',
yConfig: [
{
forAccessor: '9f69a7b0-34b9-4b76-9ff7-26dc1a06ec14',
color: euiTheme.colors.danger,
color: !!showErrorRateAsLine ? euiTheme.colors.primary : euiTheme.colors.danger,
},
],
},
{
layerId: '34298f84-681e-4fa3-8107-d6facb32ed92',
layerType: 'referenceLine',
accessors: [
'0a42b72b-cd5a-4d59-81ec-847d97c268e6',
'76d3bcc9-7d45-4b08-b2b1-8d3866ca0762',
'c531a6b1-70dd-4918-bdd0-a21535a7af05',
'61f9e663-10eb-41f7-b584-1f0f95418489',
],
accessors: ['0a42b72b-cd5a-4d59-81ec-847d97c268e6'],
yConfig: [
{
forAccessor: '0a42b72b-cd5a-4d59-81ec-847d97c268e6',
@ -86,29 +99,75 @@ export function useLensDefinition(slo: SLOResponse): TypedLensByValueInput['attr
color: euiTheme.colors.danger,
iconPosition: 'right',
},
{
forAccessor: '76d3bcc9-7d45-4b08-b2b1-8d3866ca0762',
axisMode: 'left',
textVisibility: true,
color: euiTheme.colors.danger,
iconPosition: 'right',
},
{
forAccessor: 'c531a6b1-70dd-4918-bdd0-a21535a7af05',
axisMode: 'left',
textVisibility: true,
color: euiTheme.colors.danger,
iconPosition: 'right',
},
{
forAccessor: '61f9e663-10eb-41f7-b584-1f0f95418489',
axisMode: 'left',
textVisibility: true,
color: euiTheme.colors.danger,
iconPosition: 'right',
},
],
},
...(!!alertTimeRange
? [
{
layerId: uuidv4(),
layerType: 'annotations',
annotations: [
{
type: 'manual',
id: uuidv4(),
label: i18n.translate('xpack.observability.slo.errorRateChart.alertLabel', {
defaultMessage: 'Alert',
}),
key: {
type: 'point_in_time',
timestamp: moment(alertTimeRange.from).toISOString(),
},
lineWidth: 2,
color: euiTheme.colors.danger,
icon: 'alert',
},
{
type: 'manual',
label: i18n.translate(
'xpack.observability.slo.errorRateChart.activeAlertLabel',
{
defaultMessage: 'Active alert',
}
),
key: {
type: 'range',
timestamp: moment(alertTimeRange.from).toISOString(),
endTimestamp: moment(alertTimeRange.to).toISOString(),
},
id: uuidv4(),
color: transparentize(euiTheme.colors.danger, 0.2),
},
],
ignoreGlobalFilters: true,
persistanceType: 'byValue',
},
]
: []),
...(!!annotations && annotations.length > 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,

View file

@ -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 },

View file

@ -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 },

View file

@ -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) {
</EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem>
<BurnRates slo={slo} isAutoRefreshing={isAutoRefreshing} />
<BurnRates
slo={slo}
isAutoRefreshing={isAutoRefreshing}
burnRateOptions={BURN_RATE_OPTIONS}
/>
</EuiFlexItem>
<EuiFlexItem>
<SliChartPanel

View file

@ -112,6 +112,9 @@ export const registerObservabilityRuleTypes = async (
requiresAppContext: false,
defaultActionMessage: sloBurnRateDefaultActionMessage,
defaultRecoveryMessage: sloBurnRateDefaultRecoveryMessage,
alertDetailsAppSection: lazy(
() => import('../components/slo/burn_rate/alert_details/alert_details_app_section')
),
priority: 100,
});

View file

@ -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

View file

@ -164,6 +164,7 @@ export const getRuleExecutor = ({
sloId: slo.id,
sloName: slo.name,
sloInstanceId: instanceId,
slo,
};
alert.scheduleActions(windowDef.actionGroup, context);

View file

@ -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é.",

View file

@ -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を削除した後、復元することはできません。",

View file

@ -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 删除后无法恢复。",