mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SLO] Add burn rate windows to SLO detail page (#159750)
## Summary This PR adds a burn rate visualization to the overview tab of the SLO Detail page. This PR also includes a fix for fetching the index pattern fields hook; it uses the DataViews service to fetch the fields instead of the internal API. <img width="1170" alt="image" src="41057791
-880e-4cc8-a0c7-02a0f18aaeca"> ### All good <img width="1141" alt="image" src="3ec07efa
-e35a-4251-87f3-7ddc836171b7"> ### Degrading <img width="1141" alt="image" src="a6d347be
-7b55-404e-99a1-14ad4a38ad36"> ### EVERYTHING IS BURNING 🔥 <img width="1141" alt="image" src="9ed05875
-b907-4a57-8387-a094876dd35e"> ### Recovering in the dark <img width="1151" alt="image" src="f2999c7a
-f97b-474c-8146-4565445df892"> ### No data <img width="1141" alt="image" src="675a65a4
-91b1-4de3-9f51-b65760efbb66">
This commit is contained in:
parent
7153359cb8
commit
c0d3a93dff
9 changed files with 534 additions and 28 deletions
|
@ -22,6 +22,9 @@ import {
|
|||
summarySchema,
|
||||
tagsSchema,
|
||||
timeWindowSchema,
|
||||
apmTransactionErrorRateIndicatorSchema,
|
||||
apmTransactionDurationIndicatorSchema,
|
||||
durationType,
|
||||
timeWindowTypeSchema,
|
||||
} from '../schema';
|
||||
|
||||
|
@ -141,6 +144,28 @@ const getSLODiagnosisParamsSchema = t.type({
|
|||
path: t.type({ id: t.string }),
|
||||
});
|
||||
|
||||
const getSLOBurnRatesResponseSchema = t.type({
|
||||
burnRates: t.array(
|
||||
t.type({
|
||||
name: t.string,
|
||||
burnRate: t.number,
|
||||
sli: t.number,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const getSLOBurnRatesParamsSchema = t.type({
|
||||
path: t.type({ id: t.string }),
|
||||
body: t.type({
|
||||
windows: t.array(
|
||||
t.type({
|
||||
name: t.string,
|
||||
duration: durationType,
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
type SLOResponse = t.OutputOf<typeof sloResponseSchema>;
|
||||
type SLOWithSummaryResponse = t.OutputOf<typeof sloWithSummaryResponseSchema>;
|
||||
|
||||
|
@ -166,6 +191,11 @@ type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
|
|||
type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
|
||||
type GetPreviewDataResponse = t.TypeOf<typeof getPreviewDataResponseSchema>;
|
||||
|
||||
type APMTransactionErrorRateIndicatorSchema = t.TypeOf<
|
||||
typeof apmTransactionErrorRateIndicatorSchema
|
||||
>;
|
||||
type APMTransactionDurationIndicatorSchema = t.TypeOf<typeof apmTransactionDurationIndicatorSchema>;
|
||||
type GetSLOBurnRatesResponse = t.OutputOf<typeof getSLOBurnRatesResponseSchema>;
|
||||
type BudgetingMethod = t.TypeOf<typeof budgetingMethodSchema>;
|
||||
type TimeWindow = t.TypeOf<typeof timeWindowTypeSchema>;
|
||||
|
||||
|
@ -190,6 +220,8 @@ export {
|
|||
sloWithSummaryResponseSchema,
|
||||
updateSLOParamsSchema,
|
||||
updateSLOResponseSchema,
|
||||
getSLOBurnRatesParamsSchema,
|
||||
getSLOBurnRatesResponseSchema,
|
||||
};
|
||||
export type {
|
||||
BudgetingMethod,
|
||||
|
@ -210,6 +242,9 @@ export type {
|
|||
UpdateSLOInput,
|
||||
UpdateSLOParams,
|
||||
UpdateSLOResponse,
|
||||
APMTransactionDurationIndicatorSchema,
|
||||
APMTransactionErrorRateIndicatorSchema,
|
||||
GetSLOBurnRatesResponse,
|
||||
Indicator,
|
||||
MetricCustomIndicator,
|
||||
KQLCustomIndicator,
|
||||
|
|
|
@ -33,6 +33,7 @@ export const sloKeys = {
|
|||
historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const,
|
||||
historicalSummary: (sloIds: string[]) => [...sloKeys.historicalSummaries(), sloIds] as const,
|
||||
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
|
||||
burnRates: (sloId: string) => [...sloKeys.all, 'burnRates', sloId] as const,
|
||||
preview: (indicator?: Indicator) => [...sloKeys.all, 'preview', indicator] as const,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 {
|
||||
QueryObserverResult,
|
||||
RefetchOptions,
|
||||
RefetchQueryFilters,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { GetSLOBurnRatesResponse } from '@kbn/slo-schema';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { sloKeys } from './query_key_factory';
|
||||
|
||||
export interface UseFetchSloBurnRatesResponse {
|
||||
isInitialLoading: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: GetSLOBurnRatesResponse | undefined;
|
||||
refetch: <TPageData>(
|
||||
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
|
||||
) => Promise<QueryObserverResult<GetSLOBurnRatesResponse | undefined, unknown>>;
|
||||
}
|
||||
|
||||
const LONG_REFETCH_INTERVAL = 1000 * 60; // 1 minute
|
||||
|
||||
interface UseFetchSloBurnRatesParams {
|
||||
sloId: string;
|
||||
windows: Array<{ name: string; duration: string }>;
|
||||
shouldRefetch?: boolean;
|
||||
}
|
||||
|
||||
export function useFetchSloBurnRates({
|
||||
sloId,
|
||||
windows,
|
||||
shouldRefetch,
|
||||
}: UseFetchSloBurnRatesParams): UseFetchSloBurnRatesResponse {
|
||||
const { http } = useKibana().services;
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery(
|
||||
{
|
||||
queryKey: sloKeys.burnRates(sloId),
|
||||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
const response = await http.post<GetSLOBurnRatesResponse>(
|
||||
`/internal/observability/slos/${sloId}/_burn_rates`,
|
||||
{
|
||||
body: JSON.stringify({ windows }),
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// ignore error
|
||||
}
|
||||
},
|
||||
refetchInterval: shouldRefetch ? LONG_REFETCH_INTERVAL : undefined,
|
||||
refetchOnWindowFocus: false,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
refetch,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isInitialLoading,
|
||||
isSuccess,
|
||||
isError,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiPanel,
|
||||
EuiFlexItem,
|
||||
EuiStat,
|
||||
EuiTextColor,
|
||||
EuiText,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export interface BurnRateWindowParams {
|
||||
title: string;
|
||||
target: number;
|
||||
longWindow: {
|
||||
label: string;
|
||||
burnRate: number | null;
|
||||
sli: number | null;
|
||||
};
|
||||
shortWindow: {
|
||||
label: string;
|
||||
burnRate: number | null;
|
||||
sli: number | null;
|
||||
};
|
||||
isLoading?: boolean;
|
||||
size?: 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l';
|
||||
}
|
||||
|
||||
const SUBDUED = 'subdued';
|
||||
const DANGER = 'danger';
|
||||
const SUCCESS = 'success';
|
||||
const WARNING = 'warning';
|
||||
|
||||
function getColorBasedOnBurnRate(target: number, burnRate: number | null, sli: number | null) {
|
||||
if (burnRate === null || sli === null || sli < 0) {
|
||||
return SUBDUED;
|
||||
}
|
||||
if (burnRate > target) {
|
||||
return DANGER;
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
export function BurnRateWindow({
|
||||
title,
|
||||
target,
|
||||
longWindow,
|
||||
shortWindow,
|
||||
isLoading,
|
||||
size = 's',
|
||||
}: BurnRateWindowParams) {
|
||||
const longWindowColor = getColorBasedOnBurnRate(target, longWindow.burnRate, longWindow.sli);
|
||||
const shortWindowColor = getColorBasedOnBurnRate(target, shortWindow.burnRate, shortWindow.sli);
|
||||
|
||||
const overallColor =
|
||||
longWindowColor === DANGER && shortWindowColor === DANGER
|
||||
? DANGER
|
||||
: [longWindowColor, shortWindowColor].includes(DANGER)
|
||||
? WARNING
|
||||
: longWindowColor === SUBDUED && shortWindowColor === SUBDUED
|
||||
? SUBDUED
|
||||
: SUCCESS;
|
||||
|
||||
const isLongWindowValid =
|
||||
longWindow.burnRate != null && longWindow.sli != null && longWindow.sli >= 0;
|
||||
|
||||
const isShortWindowValid =
|
||||
shortWindow.burnRate != null && shortWindow.sli != null && shortWindow.sli >= 0;
|
||||
|
||||
return (
|
||||
<EuiPanel color={overallColor}>
|
||||
<EuiText color={overallColor}>
|
||||
<h5>
|
||||
{title}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.observability.slo.burnRateWindow.thresholdTip', {
|
||||
defaultMessage: 'Threshold is {target}x',
|
||||
values: { target },
|
||||
})}
|
||||
position="top"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={isLongWindowValid ? `${numeral(longWindow.burnRate).format('0.[00]')}x` : '--'}
|
||||
titleColor={longWindowColor}
|
||||
titleSize={size}
|
||||
textAlign="left"
|
||||
isLoading={isLoading}
|
||||
description={
|
||||
<EuiTextColor color={longWindowColor}>
|
||||
<span>{longWindow.label}</span>
|
||||
</EuiTextColor>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
title={isShortWindowValid ? `${numeral(shortWindow.burnRate).format('0.[00]')}x` : '--'}
|
||||
titleColor={shortWindowColor}
|
||||
titleSize={size}
|
||||
textAlign="left"
|
||||
isLoading={isLoading}
|
||||
description={
|
||||
<EuiTextColor color={shortWindowColor}>
|
||||
<span>{shortWindow.label}</span>
|
||||
</EuiTextColor>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { GetSLOBurnRatesResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiFlexGrid,
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiBetaBadge,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useFetchSloBurnRates } from '../../../hooks/slo/use_fetch_slo_burn_rates';
|
||||
import { BurnRateWindow, BurnRateWindowParams } from './burn_rate_window';
|
||||
|
||||
interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
isAutoRefreshing?: boolean;
|
||||
}
|
||||
|
||||
const CRITICAL_LONG = 'CRITICAL_LONG';
|
||||
const CRITICAL_SHORT = 'CRITICAL_SHORT';
|
||||
const HIGH_LONG = 'HIGH_LONG';
|
||||
const HIGH_SHORT = 'HIGH_SHORT';
|
||||
const MEDIUM_LONG = 'MEDIUM_LONG';
|
||||
const MEDIUM_SHORT = 'MEDIUM_SHORT';
|
||||
const LOW_LONG = 'LOW_LONG';
|
||||
const LOW_SHORT = 'LOW_SHORT';
|
||||
|
||||
const WINDOWS = [
|
||||
{ name: CRITICAL_LONG, duration: '1h' },
|
||||
{ name: CRITICAL_SHORT, duration: '5m' },
|
||||
{ name: HIGH_LONG, duration: '6h' },
|
||||
{ name: HIGH_SHORT, duration: '30m' },
|
||||
{ name: MEDIUM_LONG, duration: '24h' },
|
||||
{ name: MEDIUM_SHORT, duration: '120m' },
|
||||
{ name: LOW_LONG, duration: '72h' },
|
||||
{ name: LOW_SHORT, duration: '360m' },
|
||||
];
|
||||
|
||||
function getSliAndBurnRate(name: string, burnRates: GetSLOBurnRatesResponse['burnRates']) {
|
||||
const data = burnRates.find((rate) => rate.name === name);
|
||||
if (!data) {
|
||||
return { burnRate: null, sli: null };
|
||||
}
|
||||
return { burnRate: data.burnRate, sli: data.sli };
|
||||
}
|
||||
|
||||
export function BurnRates({ slo, isAutoRefreshing }: Props) {
|
||||
const { isLoading, data } = useFetchSloBurnRates({
|
||||
sloId: slo.id,
|
||||
shouldRefetch: isAutoRefreshing,
|
||||
windows: WINDOWS,
|
||||
});
|
||||
|
||||
const criticalWindowParams: BurnRateWindowParams = {
|
||||
title: i18n.translate('xpack.observability.slo.burnRate.criticalTitle', {
|
||||
defaultMessage: 'Critical burn rate',
|
||||
}),
|
||||
target: 14.4,
|
||||
longWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.criticalLongLabel', {
|
||||
defaultMessage: '1 hour',
|
||||
}),
|
||||
...getSliAndBurnRate(CRITICAL_LONG, data?.burnRates ?? []),
|
||||
},
|
||||
shortWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.criticalShortLabel', {
|
||||
defaultMessage: '5 minute',
|
||||
}),
|
||||
...getSliAndBurnRate(CRITICAL_SHORT, data?.burnRates ?? []),
|
||||
},
|
||||
};
|
||||
|
||||
const highWindowParams: BurnRateWindowParams = {
|
||||
title: i18n.translate('xpack.observability.slo.burnRate.highTitle', {
|
||||
defaultMessage: 'High burn rate',
|
||||
}),
|
||||
target: 6,
|
||||
longWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.highLongLabel', {
|
||||
defaultMessage: '6 hour',
|
||||
}),
|
||||
...getSliAndBurnRate(HIGH_LONG, data?.burnRates ?? []),
|
||||
},
|
||||
shortWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.highShortLabel', {
|
||||
defaultMessage: '30 minute',
|
||||
}),
|
||||
...getSliAndBurnRate(HIGH_SHORT, data?.burnRates ?? []),
|
||||
},
|
||||
};
|
||||
|
||||
const mediumWindowParams: BurnRateWindowParams = {
|
||||
title: i18n.translate('xpack.observability.slo.burnRate.mediumTitle', {
|
||||
defaultMessage: 'Medium burn rate',
|
||||
}),
|
||||
target: 3,
|
||||
longWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.mediumLongLabel', {
|
||||
defaultMessage: '24 hours',
|
||||
}),
|
||||
...getSliAndBurnRate(MEDIUM_LONG, data?.burnRates ?? []),
|
||||
},
|
||||
shortWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.mediumShortLabel', {
|
||||
defaultMessage: '2 hours',
|
||||
}),
|
||||
...getSliAndBurnRate(MEDIUM_SHORT, data?.burnRates ?? []),
|
||||
},
|
||||
};
|
||||
|
||||
const lowWindowParams: BurnRateWindowParams = {
|
||||
title: i18n.translate('xpack.observability.slo.burnRate.lowTitle', {
|
||||
defaultMessage: 'Low burn rate',
|
||||
}),
|
||||
target: 1,
|
||||
longWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.lowLongLabel', {
|
||||
defaultMessage: '3 days',
|
||||
}),
|
||||
...getSliAndBurnRate(LOW_LONG, data?.burnRates ?? []),
|
||||
},
|
||||
shortWindow: {
|
||||
label: i18n.translate('xpack.observability.slo.burnRate.lowShortLabel', {
|
||||
defaultMessage: '6 hours',
|
||||
}),
|
||||
...getSliAndBurnRate(LOW_SHORT, data?.burnRates ?? []),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="burnRatePanel">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.slo.burnRate.title', {
|
||||
defaultMessage: 'Burn rate windows',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('xpack.observability.slo.burnRate.technicalPreviewBadgeTitle', {
|
||||
defaultMessage: 'Technical Preview',
|
||||
})}
|
||||
size="s"
|
||||
tooltipPosition="bottom"
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.observability.slo.burnRate.technicalPreviewBadgeDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'This functionality is in technical preview and is subject to change or may be removed in future versions. The design and code is less mature than official generally available features and is being provided as-is with no warranties. Technical preview features are not subject to the support service level agreement of official generally available features.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGrid columns={4}>
|
||||
<BurnRateWindow {...criticalWindowParams} isLoading={isLoading} />
|
||||
<BurnRateWindow {...highWindowParams} isLoading={isLoading} />
|
||||
<BurnRateWindow {...mediumWindowParams} isLoading={isLoading} />
|
||||
<BurnRateWindow {...lowWindowParams} isLoading={isLoading} />
|
||||
</EuiFlexGrid>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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, EuiSpacer } from '@elastic/eui';
|
||||
import React, { Fragment } from 'react';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
const ALERTS_TABLE_ID = 'xpack.observability.slo.sloDetails.alertTable';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
}
|
||||
|
||||
export function SloDetailsAlerts({ slo }: Props) {
|
||||
const {
|
||||
triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable },
|
||||
} = useKibana().services;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup direction="column" gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<AlertsStateTable
|
||||
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
|
||||
configurationId={AlertConsumers.OBSERVABILITY}
|
||||
id={ALERTS_TABLE_ID}
|
||||
flyoutSize="s"
|
||||
data-test-subj="alertTable"
|
||||
featureIds={[AlertConsumers.SLO]}
|
||||
query={{ bool: { filter: { term: { 'slo.id': slo.id } } } }}
|
||||
showExpandToDetails={false}
|
||||
showAlertStatusWithFlapping
|
||||
pageSize={100}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -15,30 +15,25 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React, { Fragment } from 'react';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary';
|
||||
import { ErrorBudgetChartPanel } from './error_budget_chart_panel';
|
||||
import { Overview as Overview } from './overview';
|
||||
import { SliChartPanel } from './sli_chart_panel';
|
||||
import { SloDetailsAlerts } from './slo_detail_alerts';
|
||||
import { BurnRates } from './burn_rates';
|
||||
|
||||
export interface Props {
|
||||
slo: SLOWithSummaryResponse;
|
||||
isAutoRefreshing: boolean;
|
||||
}
|
||||
const ALERTS_TABLE_ID = 'xpack.observability.slo.sloDetails.alertTable';
|
||||
const OVERVIEW_TAB = 'overview';
|
||||
const ALERTS_TAB = 'alerts';
|
||||
|
||||
export function SloDetails({ slo, isAutoRefreshing }: Props) {
|
||||
const {
|
||||
triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable },
|
||||
} = useKibana().services;
|
||||
|
||||
const { data: activeAlerts } = useFetchActiveAlerts({
|
||||
sloIds: [slo.id],
|
||||
});
|
||||
|
@ -66,6 +61,9 @@ export function SloDetails({ slo, isAutoRefreshing }: Props) {
|
|||
<Overview slo={slo} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<BurnRates slo={slo} isAutoRefreshing={isAutoRefreshing} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SliChartPanel
|
||||
data={historicalSliData}
|
||||
|
@ -96,27 +94,7 @@ export function SloDetails({ slo, isAutoRefreshing }: Props) {
|
|||
{(activeAlerts && activeAlerts[slo.id]?.count) ?? 0}
|
||||
</EuiNotificationBadge>
|
||||
),
|
||||
content: (
|
||||
<Fragment>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup direction="column" gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<AlertsStateTable
|
||||
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
|
||||
configurationId={AlertConsumers.OBSERVABILITY}
|
||||
id={ALERTS_TABLE_ID}
|
||||
flyoutSize="s"
|
||||
data-test-subj="alertTable"
|
||||
featureIds={[AlertConsumers.SLO]}
|
||||
query={{ bool: { filter: { term: { 'slo.id': slo.id } } } }}
|
||||
showExpandToDetails={false}
|
||||
showAlertStatusWithFlapping
|
||||
pageSize={100}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
),
|
||||
content: <SloDetailsAlerts slo={slo} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
deleteSLOParamsSchema,
|
||||
fetchHistoricalSummaryParamsSchema,
|
||||
findSLOParamsSchema,
|
||||
getSLOBurnRatesParamsSchema,
|
||||
getPreviewDataParamsSchema,
|
||||
getSLODiagnosisParamsSchema,
|
||||
getSLOParamsSchema,
|
||||
|
@ -42,6 +43,7 @@ import type { IndicatorTypes } from '../../domain/models';
|
|||
import type { ObservabilityRequestHandlerContext } from '../../types';
|
||||
import { ManageSLO } from '../../services/slo/manage_slo';
|
||||
import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis';
|
||||
import { getBurnRates } from '../../services/slo/get_burn_rates';
|
||||
import { GetPreviewData } from '../../services/slo/get_preview_data';
|
||||
|
||||
const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
|
||||
|
@ -305,6 +307,29 @@ const getSloDiagnosisRoute = createObservabilityServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const getSloBurnRates = createObservabilityServerRoute({
|
||||
endpoint: 'POST /internal/observability/slos/{id}/_burn_rates',
|
||||
options: {
|
||||
tags: ['access:slo_read'],
|
||||
},
|
||||
params: getSLOBurnRatesParamsSchema,
|
||||
handler: async ({ context, params }) => {
|
||||
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
|
||||
|
||||
if (!hasCorrectLicense) {
|
||||
throw badRequest('Platinum license or higher is needed to make use of this feature.');
|
||||
}
|
||||
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const burnRates = await getBurnRates(params.path.id, params.body.windows, {
|
||||
soClient,
|
||||
esClient,
|
||||
});
|
||||
return { burnRates };
|
||||
},
|
||||
});
|
||||
|
||||
const getPreviewData = createObservabilityServerRoute({
|
||||
endpoint: 'POST /internal/observability/slos/_preview',
|
||||
options: {
|
||||
|
@ -335,5 +360,6 @@ export const sloRouteRepository = {
|
|||
...updateSLORoute,
|
||||
...getDiagnosisRoute,
|
||||
...getSloDiagnosisRoute,
|
||||
...getSloBurnRates,
|
||||
...getPreviewData,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { KibanaSavedObjectsSLORepository } from './slo_repository';
|
||||
import { DefaultSLIClient } from './sli_client';
|
||||
import { Duration } from '../../domain/models';
|
||||
import { computeSLI, computeBurnRate } from '../../domain/services';
|
||||
|
||||
interface Services {
|
||||
soClient: SavedObjectsClientContract;
|
||||
esClient: ElasticsearchClient;
|
||||
}
|
||||
|
||||
interface LookbackWindow {
|
||||
name: string;
|
||||
duration: Duration;
|
||||
}
|
||||
|
||||
export async function getBurnRates(sloId: string, windows: LookbackWindow[], services: Services) {
|
||||
const { soClient, esClient } = services;
|
||||
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
const sliClient = new DefaultSLIClient(esClient);
|
||||
const slo = await repository.findById(sloId);
|
||||
|
||||
const sliData = await sliClient.fetchSLIDataFrom(slo, windows);
|
||||
return Object.keys(sliData).map((key) => {
|
||||
return {
|
||||
name: key,
|
||||
burnRate: computeBurnRate(slo, sliData[key]),
|
||||
sli: computeSLI(sliData[key].good, sliData[key].total),
|
||||
};
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue