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] Enable timeslice metric visualization on SLO detail page #175281

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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