Skip to content

Commit

Permalink
feat(slo): Show SLI preview chart for custom kql (#159713)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdelemme authored Jun 15, 2023
1 parent fa98aa4 commit f9d16e1
Show file tree
Hide file tree
Showing 13 changed files with 335 additions and 29 deletions.
39 changes: 24 additions & 15 deletions x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,22 @@
*/

import * as t from 'io-ts';

import {
budgetingMethodSchema,
dateType,
historicalSummarySchema,
indicatorSchema,
indicatorTypesArraySchema,
kqlCustomIndicatorSchema,
metricCustomIndicatorSchema,
objectiveSchema,
optionalSettingsSchema,
previewDataSchema,
settingsSchema,
sloIdSchema,
summarySchema,
tagsSchema,
timeWindowSchema,
metricCustomIndicatorSchema,
kqlCustomIndicatorSchema,
apmTransactionErrorRateIndicatorSchema,
apmTransactionDurationIndicatorSchema,
} from '../schema';

const createSLOParamsSchema = t.type({
Expand All @@ -44,6 +42,14 @@ const createSLOResponseSchema = t.type({
id: sloIdSchema,
});

const getPreviewDataParamsSchema = t.type({
body: t.type({
indicator: indicatorSchema,
}),
});

const getPreviewDataResponseSchema = t.array(previewDataSchema);

const deleteSLOParamsSchema = t.type({
path: t.type({
id: sloIdSchema,
Expand Down Expand Up @@ -156,20 +162,22 @@ type FetchHistoricalSummaryParams = t.TypeOf<typeof fetchHistoricalSummaryParams
type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>;
type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;

type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
type GetPreviewDataResponse = t.TypeOf<typeof getPreviewDataResponseSchema>;

type BudgetingMethod = t.TypeOf<typeof budgetingMethodSchema>;

type MetricCustomIndicatorSchema = t.TypeOf<typeof metricCustomIndicatorSchema>;
type KQLCustomIndicatorSchema = t.TypeOf<typeof kqlCustomIndicatorSchema>;
type APMTransactionErrorRateIndicatorSchema = t.TypeOf<
typeof apmTransactionErrorRateIndicatorSchema
>;
type APMTransactionDurationIndicatorSchema = t.TypeOf<typeof apmTransactionDurationIndicatorSchema>;
type Indicator = t.OutputOf<typeof indicatorSchema>;
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
type KQLCustomIndicator = t.OutputOf<typeof kqlCustomIndicatorSchema>;

export {
createSLOParamsSchema,
deleteSLOParamsSchema,
findSLOParamsSchema,
findSLOResponseSchema,
getPreviewDataParamsSchema,
getPreviewDataResponseSchema,
getSLODiagnosisParamsSchema,
getSLOParamsSchema,
getSLOResponseSchema,
Expand All @@ -188,6 +196,8 @@ export type {
CreateSLOResponse,
FindSLOParams,
FindSLOResponse,
GetPreviewDataParams,
GetPreviewDataResponse,
GetSLOResponse,
FetchHistoricalSummaryParams,
FetchHistoricalSummaryResponse,
Expand All @@ -198,8 +208,7 @@ export type {
UpdateSLOInput,
UpdateSLOParams,
UpdateSLOResponse,
MetricCustomIndicatorSchema,
KQLCustomIndicatorSchema,
APMTransactionDurationIndicatorSchema,
APMTransactionErrorRateIndicatorSchema,
Indicator,
MetricCustomIndicator,
KQLCustomIndicator,
};
6 changes: 6 additions & 0 deletions x-pack/packages/kbn-slo-schema/src/schema/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const historicalSummarySchema = t.intersection([
summarySchema,
]);

const previewDataSchema = t.type({
date: dateType,
sliValue: t.number,
});

const dateRangeSchema = t.type({ from: dateType, to: dateType });

export type { SummarySchema };
Expand All @@ -63,6 +68,7 @@ export {
dateType,
errorBudgetSchema,
historicalSummarySchema,
previewDataSchema,
statusSchema,
summarySchema,
};
4 changes: 2 additions & 2 deletions x-pack/plugins/observability/public/data/slo/indicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { KQLCustomIndicatorSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { KQLCustomIndicator, SLOWithSummaryResponse } from '@kbn/slo-schema';

export const buildApmAvailabilityIndicator = (
params: Partial<SLOWithSummaryResponse['indicator']['params']> = {}
Expand Down Expand Up @@ -53,5 +53,5 @@ export const buildCustomKqlIndicator = (
timestampField: '@timestamp',
...params,
},
} as KQLCustomIndicatorSchema;
} as KQLCustomIndicator;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import type { Indicator } from '@kbn/slo-schema';

interface SloKeyFilter {
name: string;
page: number;
Expand All @@ -31,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,
preview: (indicator?: Indicator) => [...sloKeys.all, 'preview', indicator] as const,
};

export const compositeSloKeys = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { GetPreviewDataResponse, Indicator } from '@kbn/slo-schema';
import {
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
useQuery,
} from '@tanstack/react-query';
import { useKibana } from '../../utils/kibana_react';
import { sloKeys } from './query_key_factory';

export interface UseGetPreviewData {
data: GetPreviewDataResponse | undefined;
isInitialLoading: boolean;
isRefetching: boolean;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
refetch: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<GetPreviewDataResponse | undefined, unknown>>;
}

export function useGetPreviewData(indicator?: Indicator): UseGetPreviewData {
const { http } = useKibana().services;

const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery(
{
queryKey: sloKeys.preview(indicator),
queryFn: async ({ signal }) => {
const response = await http.post<GetPreviewDataResponse>(
'/internal/observability/slos/_preview',
{
body: JSON.stringify({ indicator }),
signal,
}
);

return response;
},
retry: false,
refetchOnWindowFocus: false,
enabled: Boolean(indicator),
}
);

return {
data,
isLoading,
isRefetching,
isInitialLoading,
isSuccess,
isError,
refetch,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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 { AreaSeries, Axis, Chart, Position, ScaleType, Settings } from '@elastic/charts';
import { EuiFlexItem, EuiIcon, EuiLoadingChart, EuiPanel } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { CreateSLOInput } from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { useKibana } from '../../../../utils/kibana_react';
import { useDebouncedGetPreviewData } from '../../hooks/use_preview';
export function DataPreviewChart() {
const { watch, getFieldState } = useFormContext<CreateSLOInput>();
const { charts, uiSettings } = useKibana().services;

const { data: previewData, isLoading: isPreviewLoading } = useDebouncedGetPreviewData(
watch('indicator')
);

const theme = charts.theme.useChartsTheme();
const baseTheme = charts.theme.useChartsBaseTheme();
const dateFormat = uiSettings.get('dateFormat');
const percentFormat = uiSettings.get('format:percent:defaultPattern');

if (getFieldState('indicator').invalid) {
return null;
}

return (
<EuiFlexItem>
{isPreviewLoading && <EuiLoadingChart size="m" mono />}
{!isPreviewLoading && !!previewData && (
<EuiPanel hasBorder={true} hasShadow={false}>
<Chart size={{ height: 160, width: '100%' }}>
<Settings
baseTheme={baseTheme}
showLegend={false}
theme={[
{
...theme,
lineSeriesStyle: {
point: { visible: false },
},
},
]}
tooltip="vertical"
noResults={
<EuiIcon type="visualizeApp" size="l" color="subdued" title="no results" />
}
/>

<Axis
id="y-axis"
title={i18n.translate('xpack.observability.slo.sloEdit.dataPreviewChart.yTitle', {
defaultMessage: 'SLI',
})}
ticks={5}
position={Position.Left}
tickFormat={(d) => numeral(d).format(percentFormat)}
/>

<Axis
id="time"
title={i18n.translate('xpack.observability.slo.sloEdit.dataPreviewChart.xTitle', {
defaultMessage: 'Last hour',
})}
tickFormat={(d) => moment(d).format(dateFormat)}
position={Position.Bottom}
timeAxisLayerCount={2}
gridLine={{ visible: true }}
style={{
tickLine: { size: 0.0001, padding: 4, visible: true },
tickLabel: {
alignment: {
horizontal: Position.Left,
vertical: Position.Bottom,
},
padding: 0,
offset: { x: 0, y: 0 },
},
}}
/>
<AreaSeries
id="SLI"
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="date"
yAccessors={['value']}
data={previewData.map((datum) => ({
date: new Date(datum.date).getTime(),
value: datum.sliValue >= 0 ? datum.sliValue : null,
}))}
/>
</Chart>
</EuiPanel>
)}
</EuiFlexItem>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Field,
useFetchIndexPatternFields,
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
import { DataPreviewChart } from '../common/data_preview_chart';
import { QueryBuilder } from '../common/query_builder';
import { IndexSelection } from '../custom_common/index_selection';

Expand All @@ -31,7 +32,6 @@ interface Option {

export function CustomKqlIndicatorTypeForm() {
const { control, watch, getFieldState } = useFormContext<CreateSLOInput>();

const { isLoading, data: indexFields } = useFetchIndexPatternFields(
watch('indicator.params.index')
);
Expand Down Expand Up @@ -86,12 +86,7 @@ export function CustomKqlIndicatorTypeForm() {
!!watch('indicator.params.index') &&
!!field.value &&
timestampFields.some((timestampField) => timestampField.name === field.value)
? [
{
value: field.value,
label: field.value,
},
]
? [{ value: field.value, label: field.value }]
: []
}
singleSelection={{ asPlainText: true }}
Expand Down Expand Up @@ -187,6 +182,8 @@ export function CustomKqlIndicatorTypeForm() {
}
/>
</EuiFlexItem>

<DataPreviewChart />
</EuiFlexGroup>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { Indicator } from '@kbn/slo-schema';
import { debounce } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { useGetPreviewData } from '../../../hooks/slo/use_get_preview_data';

export function useDebouncedGetPreviewData(indicator: Indicator) {
const serializedIndicator = JSON.stringify(indicator);
const [indicatorState, setIndicatorState] = useState<string>(serializedIndicator);

// eslint-disable-next-line react-hooks/exhaustive-deps
const store = useCallback(
debounce((value: string) => setIndicatorState(value), 800),
[]
);
useEffect(() => {
if (indicatorState !== serializedIndicator) {
store(serializedIndicator);
}
}, [indicatorState, serializedIndicator, store]);

return useGetPreviewData(JSON.parse(indicatorState));
}
Loading

0 comments on commit f9d16e1

Please sign in to comment.