Skip to content

Commit

Permalink
[SLO] Enable timeslice metric visualization on SLO detail page (#175281)
Browse files Browse the repository at this point in the history
## Summary

This PR adds support for the Timeslice Metric visualization on the SLO
Detail page.

Fixes #170135 

<img width="1756" alt="image"
src="https://github.com/elastic/kibana/assets/41702/56599b91-8827-4c6a-9df1-ccd80c5ab097">
  • Loading branch information
simianhacker authored Jan 30, 2024
1 parent f66db98 commit 134b25c
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 78 deletions.
4 changes: 4 additions & 0 deletions x-pack/packages/kbn-slo-schema/src/models/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class Duration {
asSeconds(): number {
return moment.duration(this.value, toMomentUnitOfTime(this.unit)).asSeconds();
}

asMinutes(): number {
return moment.duration(this.value, toMomentUnitOfTime(this.unit)).asMinutes();
}
}

const toDurationUnit = (unit: string): DurationUnit => {
Expand Down
19 changes: 13 additions & 6 deletions x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,18 @@ const createSLOResponseSchema = t.type({
});

const getPreviewDataParamsSchema = t.type({
body: t.type({
indicator: indicatorSchema,
range: t.type({
start: t.number,
end: t.number,
body: t.intersection([
t.type({
indicator: indicatorSchema,
range: t.type({
start: t.number,
end: t.number,
}),
}),
}),
t.partial({
objective: objectiveSchema,
}),
]),
});

const getPreviewDataResponseSchema = t.array(previewDataSchema);
Expand Down Expand Up @@ -282,6 +287,7 @@ type BudgetingMethod = t.OutputOf<typeof budgetingMethodSchema>;
type TimeWindow = t.OutputOf<typeof timeWindowTypeSchema>;
type IndicatorType = t.OutputOf<typeof indicatorTypesSchema>;
type Indicator = t.OutputOf<typeof indicatorSchema>;
type Objective = t.OutputOf<typeof objectiveSchema>;
type APMTransactionErrorRateIndicator = t.OutputOf<typeof apmTransactionErrorRateIndicatorSchema>;
type APMTransactionDurationIndicator = t.OutputOf<typeof apmTransactionDurationIndicatorSchema>;
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
Expand Down Expand Up @@ -350,6 +356,7 @@ export type {
GetSLOInstancesResponse,
IndicatorType,
Indicator,
Objective,
MetricCustomIndicator,
TimesliceMetricIndicator,
TimesliceMetricBasicMetricWithField,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { GetPreviewDataResponse, Indicator } from '@kbn/slo-schema';
import { GetPreviewDataResponse, Indicator, Objective } from '@kbn/slo-schema';
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '../../utils/kibana_react';
import { sloKeys } from './query_key_factory';
Expand All @@ -21,7 +21,9 @@ export interface UseGetPreviewData {
export function useGetPreviewData(
isValid: boolean,
indicator: Indicator,
range: { start: number; end: number }
range: { start: number; end: number },
objective?: Objective,
filter?: string
): UseGetPreviewData {
const { http } = useKibana().services;

Expand All @@ -31,7 +33,11 @@ export function useGetPreviewData(
const response = await http.post<GetPreviewDataResponse>(
'/internal/observability/slos/_preview',
{
body: JSON.stringify({ indicator, range }),
body: JSON.stringify({
indicator: { ...indicator, params: { ...indicator.params, filter } },
range,
...((objective && { objective }) || {}),
}),
signal,
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
*/

import {
AnnotationDomainType,
AreaSeries,
Axis,
BarSeries,
Chart,
LineAnnotation,
Position,
RectAnnotation,
ScaleType,
Settings,
Tooltip,
Expand All @@ -28,11 +32,13 @@ import {
import numeral from '@elastic/numeral';
import { useActiveCursor } from '@kbn/charts-plugin/public';
import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React, { useRef } from 'react';
import { max, min } from 'lodash';
import { useGetPreviewData } from '../../../hooks/slo/use_get_preview_data';
import { useKibana } from '../../../utils/kibana_react';
import { COMPARATOR_MAPPING } from '../../slo_edit/constants';

export interface Props {
slo: SLOWithSummaryResponse;
Expand All @@ -45,7 +51,8 @@ export interface Props {
export function EventsChartPanel({ slo, range }: Props) {
const { charts, uiSettings } = useKibana().services;
const { euiTheme } = useEuiTheme();
const { isLoading, data } = useGetPreviewData(true, slo.indicator, range);
const filter = slo.instanceId !== ALL_VALUE ? `${slo.groupBy}: "${slo.instanceId}"` : '';
const { isLoading, data } = useGetPreviewData(true, slo.indicator, range, slo.objective, filter);
const baseTheme = charts.theme.useChartsBaseTheme();
const chartRef = useRef(null);
const handleCursorUpdate = useActiveCursor(charts.activeCursor, chartRef, {
Expand All @@ -54,19 +61,87 @@ export function EventsChartPanel({ slo, range }: Props) {

const dateFormat = uiSettings.get('dateFormat');

const title =
slo.indicator.type !== 'sli.metric.timeslice' ? (
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.observability.slo.sloDetails.eventsChartPanel.title', {
defaultMessage: 'Good vs bad events',
})}
</h2>
</EuiTitle>
) : (
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.observability.slo.sloDetails.eventsChartPanel.timesliceTitle', {
defaultMessage: 'Timeslice metric',
})}
</h2>
</EuiTitle>
);
const threshold =
slo.indicator.type === 'sli.metric.timeslice'
? slo.indicator.params.metric.threshold
: undefined;
const yAxisNumberFormat = slo.indicator.type === 'sli.metric.timeslice' ? '0,0[.00]' : '0,0';

const values = (data || []).map((row) => {
if (slo.indicator.type === 'sli.metric.timeslice') {
return row.sliValue;
} else {
return row?.events?.total || 0;
}
});
const maxValue = max(values);
const minValue = min(values);
const domain = {
fit: true,
min:
threshold != null && minValue != null && threshold < minValue ? threshold : minValue || NaN,
max:
threshold != null && maxValue != null && threshold > maxValue ? threshold : maxValue || NaN,
};

const annotation =
slo.indicator.type === 'sli.metric.timeslice' && threshold ? (
<>
<LineAnnotation
id="thresholdAnnotation"
domainType={AnnotationDomainType.YDomain}
dataValues={[{ dataValue: threshold }]}
style={{
line: {
strokeWidth: 2,
stroke: euiTheme.colors.warning || '#000',
opacity: 1,
},
}}
marker={<span>{threshold}</span>}
markerPosition="right"
/>
<RectAnnotation
dataValues={[
{
coordinates: ['GT', 'GTE'].includes(slo.indicator.params.metric.comparator)
? {
y0: threshold,
y1: maxValue,
}
: { y0: minValue, y1: threshold },
details: `${COMPARATOR_MAPPING[slo.indicator.params.metric.comparator]} ${threshold}`,
},
]}
id="thresholdShade"
style={{ fill: euiTheme.colors.warning || '#000', opacity: 0.1 }}
/>
</>
) : null;

return (
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="eventsChartPanel">
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.observability.slo.sloDetails.eventsChartPanel.title', {
defaultMessage: 'Good vs bad events',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>{title}</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.observability.slo.sloDetails.eventsChartPanel.duration', {
Expand All @@ -84,7 +159,7 @@ export function EventsChartPanel({ slo, range }: Props) {
<Tooltip type={TooltipType.VerticalCursor} />
<Settings
baseTheme={baseTheme}
showLegend
showLegend={slo.indicator.type !== 'sli.metric.timeslice'}
showLegendExtra={false}
legendPosition={Position.Left}
noResults={
Expand All @@ -98,6 +173,7 @@ export function EventsChartPanel({ slo, range }: Props) {
pointerUpdateTrigger={'x'}
locale={i18n.getLocale()}
/>
{annotation}

<Axis
id="bottom"
Expand All @@ -108,54 +184,71 @@ export function EventsChartPanel({ slo, range }: Props) {
<Axis
id="left"
position={Position.Left}
tickFormat={(d) => numeral(d).format('0,0')}
tickFormat={(d) => numeral(d).format(yAxisNumberFormat)}
domain={domain}
/>

<BarSeries
id={i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.goodEventsLabel',
{ defaultMessage: 'Good events' }
)}
color={euiTheme.colors.success}
barSeriesStyle={{
rect: { fill: euiTheme.colors.success },
displayValue: { fill: euiTheme.colors.success },
}}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['value']}
stackAccessors={[0]}
data={
data?.map((datum) => ({
key: new Date(datum.date).getTime(),
value: datum.events?.good,
})) ?? []
}
/>
{slo.indicator.type !== 'sli.metric.timeslice' ? (
<>
<BarSeries
id={i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.goodEventsLabel',
{ defaultMessage: 'Good events' }
)}
color={euiTheme.colors.success}
barSeriesStyle={{
rect: { fill: euiTheme.colors.success },
displayValue: { fill: euiTheme.colors.success },
}}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['value']}
stackAccessors={[0]}
data={
data?.map((datum) => ({
key: new Date(datum.date).getTime(),
value: datum.events?.good,
})) ?? []
}
/>

<BarSeries
id={i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.badEventsLabel',
{ defaultMessage: 'Bad events' }
)}
color={euiTheme.colors.danger}
barSeriesStyle={{
rect: { fill: euiTheme.colors.danger },
displayValue: { fill: euiTheme.colors.danger },
}}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['value']}
stackAccessors={[0]}
data={
data?.map((datum) => ({
key: new Date(datum.date).getTime(),
value: datum.events?.bad,
})) ?? []
}
/>
<BarSeries
id={i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.badEventsLabel',
{ defaultMessage: 'Bad events' }
)}
color={euiTheme.colors.danger}
barSeriesStyle={{
rect: { fill: euiTheme.colors.danger },
displayValue: { fill: euiTheme.colors.danger },
}}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['value']}
stackAccessors={[0]}
data={
data?.map((datum) => ({
key: new Date(datum.date).getTime(),
value: datum.events?.bad,
})) ?? []
}
/>
</>
) : (
<AreaSeries
id="Metric"
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="date"
yAccessors={['value']}
data={(data ?? []).map((datum) => ({
date: new Date(datum.date).getTime(),
value: datum.sliValue >= 0 ? datum.sliValue : null,
}))}
/>
)}
</Chart>
)}
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,9 @@ export function SloDetails({ slo, isAutoRefreshing }: Props) {
slo={slo}
/>
</EuiFlexItem>
{slo.indicator.type !== 'sli.metric.timeslice' ? (
<EuiFlexItem>
<EventsChartPanel slo={slo} range={range} />
</EuiFlexItem>
) : null}
<EuiFlexItem>
<EventsChartPanel slo={slo} range={range} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface DataPreviewChartProps {
thresholdDirection?: 'above' | 'below';
thresholdColor?: string;
thresholdMessage?: string;
ignoreMoreThan100?: boolean;
}

const ONE_HOUR_IN_MILLISECONDS = 1 * 60 * 60 * 1000;
Expand All @@ -58,6 +59,7 @@ export function DataPreviewChart({
thresholdDirection,
thresholdColor,
thresholdMessage,
ignoreMoreThan100,
}: DataPreviewChartProps) {
const { watch, getFieldState, formState, getValues } = useFormContext<CreateSLOForm>();
const { charts, uiSettings } = useKibana().services;
Expand All @@ -80,7 +82,7 @@ export function DataPreviewChart({
isError,
} = useDebouncedGetPreviewData(isIndicatorSectionValid, watch('indicator'), range);

const isMoreThan100 = previewData?.find((row) => row.sliValue > 1) != null;
const isMoreThan100 = !ignoreMoreThan100 && previewData?.find((row) => row.sliValue > 1) != null;

const baseTheme = charts.theme.useChartsBaseTheme();
const dateFormat = uiSettings.get('dateFormat');
Expand Down
Loading

0 comments on commit 134b25c

Please sign in to comment.