mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
feat(slo): burn rate alert details page (#174548)
This commit is contained in:
parent
68d1bac8b8
commit
3e9cc8d692
20 changed files with 788 additions and 289 deletions
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -164,6 +164,7 @@ export const getRuleExecutor = ({
|
|||
sloId: slo.id,
|
||||
sloName: slo.name,
|
||||
sloInstanceId: instanceId,
|
||||
slo,
|
||||
};
|
||||
|
||||
alert.scheduleActions(windowDef.actionGroup, context);
|
||||
|
|
|
@ -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é.",
|
||||
|
|
|
@ -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を削除した後、復元することはできません。",
|
||||
|
|
|
@ -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 删除后无法恢复。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue