Skip to content

Commit

Permalink
[SLO] Add burn rate windows to SLO detail page (#159750)
Browse files Browse the repository at this point in the history
## 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="https://github.com/elastic/kibana/assets/41702/41057791-880e-4cc8-a0c7-02a0f18aaeca">

### All good

<img width="1141" alt="image"
src="https://github.com/elastic/kibana/assets/41702/3ec07efa-e35a-4251-87f3-7ddc836171b7">

### Degrading

<img width="1141" alt="image"
src="https://github.com/elastic/kibana/assets/41702/a6d347be-7b55-404e-99a1-14ad4a38ad36">

### EVERYTHING IS BURNING 🔥

<img width="1141" alt="image"
src="https://github.com/elastic/kibana/assets/41702/9ed05875-b907-4a57-8387-a094876dd35e">


### Recovering in the dark

<img width="1151" alt="image"
src="https://github.com/elastic/kibana/assets/41702/f2999c7a-f97b-474c-8146-4565445df892">

### No data

<img width="1141" alt="image"
src="https://github.com/elastic/kibana/assets/41702/675a65a4-91b1-4de3-9f51-b65760efbb66">
  • Loading branch information
simianhacker committed Jun 20, 2023
1 parent 7153359 commit c0d3a93
Show file tree
Hide file tree
Showing 9 changed files with 534 additions and 28 deletions.
35 changes: 35 additions & 0 deletions x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts
Expand Up @@ -22,6 +22,9 @@ import {
summarySchema,
tagsSchema,
timeWindowSchema,
apmTransactionErrorRateIndicatorSchema,
apmTransactionDurationIndicatorSchema,
durationType,
timeWindowTypeSchema,
} from '../schema';

Expand Down Expand Up @@ -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>;

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

Expand All @@ -190,6 +220,8 @@ export {
sloWithSummaryResponseSchema,
updateSLOParamsSchema,
updateSLOResponseSchema,
getSLOBurnRatesParamsSchema,
getSLOBurnRatesResponseSchema,
};
export type {
BudgetingMethod,
Expand All @@ -210,6 +242,9 @@ export type {
UpdateSLOInput,
UpdateSLOParams,
UpdateSLOResponse,
APMTransactionDurationIndicatorSchema,
APMTransactionErrorRateIndicatorSchema,
GetSLOBurnRatesResponse,
Indicator,
MetricCustomIndicator,
KQLCustomIndicator,
Expand Down
Expand Up @@ -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,
};

Expand Down
@@ -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 comments on commit c0d3a93

Please sign in to comment.