Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SLO] Add burn rate windows to SLO detail page #159750

Merged
merged 17 commits into from Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4b0c238
[SLO] Add burn rate windows to SLO detail page
simianhacker Jun 14, 2023
f9bdd64
Merge branch 'main' of github.com:elastic/kibana into slo-burn-rate-t…
simianhacker Jun 14, 2023
2a10e34
Fixing i18n keys
simianhacker Jun 14, 2023
e77e7c0
Merge branch 'main' of github.com:elastic/kibana into slo-burn-rate-t…
simianhacker Jun 14, 2023
f2d056e
Switching to DataView interface since /api/index_patterns/_fields_for…
simianhacker Jun 14, 2023
5d1d836
Refactoring the code for the burn rate colors
simianhacker Jun 15, 2023
aa4b7d2
Move to using Duration
simianhacker Jun 15, 2023
6e7a9f0
Adding access:slo-read tag to burn rate route; removing date version
simianhacker Jun 15, 2023
7315f63
Merge branch 'main' of github.com:elastic/kibana into slo-burn-rate-t…
simianhacker Jun 15, 2023
d216f89
Merge branch 'main' of github.com:elastic/kibana into slo-burn-rate-t…
simianhacker Jun 20, 2023
e532835
Fixing background color so if long is SUCCESS and short is SUBDUED, t…
simianhacker Jun 20, 2023
26ba019
reverting some changes for 0 burn rate
simianhacker Jun 20, 2023
6d83c6f
Adding technical preview badge to burn rate windows
simianhacker Jun 20, 2023
e35341e
Merge branch 'main' of github.com:elastic/kibana into slo-burn-rate-t…
simianhacker Jun 20, 2023
b6aeb6c
changing maybe to may be
simianhacker Jun 20, 2023
f1239f5
Displaying 0x when sli > 1 and burnRate != null
simianhacker Jun 20, 2023
deef9e0
Fixing burn rate colors/display when sli is ZERO
simianhacker Jun 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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),
simianhacker marked this conversation as resolved.
Show resolved Hide resolved
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>
);
}