mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
feat(slo): Show SLI preview chart for custom kql (#159713)
This commit is contained in:
parent
fa98aa4f8c
commit
f9d16e160b
13 changed files with 335 additions and 29 deletions
|
@ -6,24 +6,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as t from 'io-ts';
|
import * as t from 'io-ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
budgetingMethodSchema,
|
budgetingMethodSchema,
|
||||||
dateType,
|
dateType,
|
||||||
historicalSummarySchema,
|
historicalSummarySchema,
|
||||||
indicatorSchema,
|
indicatorSchema,
|
||||||
indicatorTypesArraySchema,
|
indicatorTypesArraySchema,
|
||||||
|
kqlCustomIndicatorSchema,
|
||||||
|
metricCustomIndicatorSchema,
|
||||||
objectiveSchema,
|
objectiveSchema,
|
||||||
optionalSettingsSchema,
|
optionalSettingsSchema,
|
||||||
|
previewDataSchema,
|
||||||
settingsSchema,
|
settingsSchema,
|
||||||
sloIdSchema,
|
sloIdSchema,
|
||||||
summarySchema,
|
summarySchema,
|
||||||
tagsSchema,
|
tagsSchema,
|
||||||
timeWindowSchema,
|
timeWindowSchema,
|
||||||
metricCustomIndicatorSchema,
|
|
||||||
kqlCustomIndicatorSchema,
|
|
||||||
apmTransactionErrorRateIndicatorSchema,
|
|
||||||
apmTransactionDurationIndicatorSchema,
|
|
||||||
} from '../schema';
|
} from '../schema';
|
||||||
|
|
||||||
const createSLOParamsSchema = t.type({
|
const createSLOParamsSchema = t.type({
|
||||||
|
@ -44,6 +42,14 @@ const createSLOResponseSchema = t.type({
|
||||||
id: sloIdSchema,
|
id: sloIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getPreviewDataParamsSchema = t.type({
|
||||||
|
body: t.type({
|
||||||
|
indicator: indicatorSchema,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPreviewDataResponseSchema = t.array(previewDataSchema);
|
||||||
|
|
||||||
const deleteSLOParamsSchema = t.type({
|
const deleteSLOParamsSchema = t.type({
|
||||||
path: t.type({
|
path: t.type({
|
||||||
id: sloIdSchema,
|
id: sloIdSchema,
|
||||||
|
@ -156,20 +162,22 @@ type FetchHistoricalSummaryParams = t.TypeOf<typeof fetchHistoricalSummaryParams
|
||||||
type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>;
|
type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>;
|
||||||
type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
|
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 BudgetingMethod = t.TypeOf<typeof budgetingMethodSchema>;
|
||||||
|
|
||||||
type MetricCustomIndicatorSchema = t.TypeOf<typeof metricCustomIndicatorSchema>;
|
type Indicator = t.OutputOf<typeof indicatorSchema>;
|
||||||
type KQLCustomIndicatorSchema = t.TypeOf<typeof kqlCustomIndicatorSchema>;
|
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
|
||||||
type APMTransactionErrorRateIndicatorSchema = t.TypeOf<
|
type KQLCustomIndicator = t.OutputOf<typeof kqlCustomIndicatorSchema>;
|
||||||
typeof apmTransactionErrorRateIndicatorSchema
|
|
||||||
>;
|
|
||||||
type APMTransactionDurationIndicatorSchema = t.TypeOf<typeof apmTransactionDurationIndicatorSchema>;
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createSLOParamsSchema,
|
createSLOParamsSchema,
|
||||||
deleteSLOParamsSchema,
|
deleteSLOParamsSchema,
|
||||||
findSLOParamsSchema,
|
findSLOParamsSchema,
|
||||||
findSLOResponseSchema,
|
findSLOResponseSchema,
|
||||||
|
getPreviewDataParamsSchema,
|
||||||
|
getPreviewDataResponseSchema,
|
||||||
getSLODiagnosisParamsSchema,
|
getSLODiagnosisParamsSchema,
|
||||||
getSLOParamsSchema,
|
getSLOParamsSchema,
|
||||||
getSLOResponseSchema,
|
getSLOResponseSchema,
|
||||||
|
@ -188,6 +196,8 @@ export type {
|
||||||
CreateSLOResponse,
|
CreateSLOResponse,
|
||||||
FindSLOParams,
|
FindSLOParams,
|
||||||
FindSLOResponse,
|
FindSLOResponse,
|
||||||
|
GetPreviewDataParams,
|
||||||
|
GetPreviewDataResponse,
|
||||||
GetSLOResponse,
|
GetSLOResponse,
|
||||||
FetchHistoricalSummaryParams,
|
FetchHistoricalSummaryParams,
|
||||||
FetchHistoricalSummaryResponse,
|
FetchHistoricalSummaryResponse,
|
||||||
|
@ -198,8 +208,7 @@ export type {
|
||||||
UpdateSLOInput,
|
UpdateSLOInput,
|
||||||
UpdateSLOParams,
|
UpdateSLOParams,
|
||||||
UpdateSLOResponse,
|
UpdateSLOResponse,
|
||||||
MetricCustomIndicatorSchema,
|
Indicator,
|
||||||
KQLCustomIndicatorSchema,
|
MetricCustomIndicator,
|
||||||
APMTransactionDurationIndicatorSchema,
|
KQLCustomIndicator,
|
||||||
APMTransactionErrorRateIndicatorSchema,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,6 +52,11 @@ const historicalSummarySchema = t.intersection([
|
||||||
summarySchema,
|
summarySchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const previewDataSchema = t.type({
|
||||||
|
date: dateType,
|
||||||
|
sliValue: t.number,
|
||||||
|
});
|
||||||
|
|
||||||
const dateRangeSchema = t.type({ from: dateType, to: dateType });
|
const dateRangeSchema = t.type({ from: dateType, to: dateType });
|
||||||
|
|
||||||
export type { SummarySchema };
|
export type { SummarySchema };
|
||||||
|
@ -63,6 +68,7 @@ export {
|
||||||
dateType,
|
dateType,
|
||||||
errorBudgetSchema,
|
errorBudgetSchema,
|
||||||
historicalSummarySchema,
|
historicalSummarySchema,
|
||||||
|
previewDataSchema,
|
||||||
statusSchema,
|
statusSchema,
|
||||||
summarySchema,
|
summarySchema,
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { KQLCustomIndicatorSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
import { KQLCustomIndicator, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||||
|
|
||||||
export const buildApmAvailabilityIndicator = (
|
export const buildApmAvailabilityIndicator = (
|
||||||
params: Partial<SLOWithSummaryResponse['indicator']['params']> = {}
|
params: Partial<SLOWithSummaryResponse['indicator']['params']> = {}
|
||||||
|
@ -53,5 +53,5 @@ export const buildCustomKqlIndicator = (
|
||||||
timestampField: '@timestamp',
|
timestampField: '@timestamp',
|
||||||
...params,
|
...params,
|
||||||
},
|
},
|
||||||
} as KQLCustomIndicatorSchema;
|
} as KQLCustomIndicator;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Indicator } from '@kbn/slo-schema';
|
||||||
|
|
||||||
interface SloKeyFilter {
|
interface SloKeyFilter {
|
||||||
name: string;
|
name: string;
|
||||||
page: number;
|
page: number;
|
||||||
|
@ -31,6 +33,7 @@ export const sloKeys = {
|
||||||
historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const,
|
historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const,
|
||||||
historicalSummary: (sloIds: string[]) => [...sloKeys.historicalSummaries(), sloIds] as const,
|
historicalSummary: (sloIds: string[]) => [...sloKeys.historicalSummaries(), sloIds] as const,
|
||||||
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
|
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
|
||||||
|
preview: (indicator?: Indicator) => [...sloKeys.all, 'preview', indicator] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const compositeSloKeys = {
|
export const compositeSloKeys = {
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import {
|
||||||
Field,
|
Field,
|
||||||
useFetchIndexPatternFields,
|
useFetchIndexPatternFields,
|
||||||
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
} from '../../../../hooks/slo/use_fetch_index_pattern_fields';
|
||||||
|
import { DataPreviewChart } from '../common/data_preview_chart';
|
||||||
import { QueryBuilder } from '../common/query_builder';
|
import { QueryBuilder } from '../common/query_builder';
|
||||||
import { IndexSelection } from '../custom_common/index_selection';
|
import { IndexSelection } from '../custom_common/index_selection';
|
||||||
|
|
||||||
|
@ -31,7 +32,6 @@ interface Option {
|
||||||
|
|
||||||
export function CustomKqlIndicatorTypeForm() {
|
export function CustomKqlIndicatorTypeForm() {
|
||||||
const { control, watch, getFieldState } = useFormContext<CreateSLOInput>();
|
const { control, watch, getFieldState } = useFormContext<CreateSLOInput>();
|
||||||
|
|
||||||
const { isLoading, data: indexFields } = useFetchIndexPatternFields(
|
const { isLoading, data: indexFields } = useFetchIndexPatternFields(
|
||||||
watch('indicator.params.index')
|
watch('indicator.params.index')
|
||||||
);
|
);
|
||||||
|
@ -86,12 +86,7 @@ export function CustomKqlIndicatorTypeForm() {
|
||||||
!!watch('indicator.params.index') &&
|
!!watch('indicator.params.index') &&
|
||||||
!!field.value &&
|
!!field.value &&
|
||||||
timestampFields.some((timestampField) => timestampField.name === field.value)
|
timestampFields.some((timestampField) => timestampField.name === field.value)
|
||||||
? [
|
? [{ value: field.value, label: field.value }]
|
||||||
{
|
|
||||||
value: field.value,
|
|
||||||
label: field.value,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
singleSelection={{ asPlainText: true }}
|
singleSelection={{ asPlainText: true }}
|
||||||
|
@ -187,6 +182,8 @@ export function CustomKqlIndicatorTypeForm() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<DataPreviewChart />
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CreateSLOInput, MetricCustomIndicatorSchema } from '@kbn/slo-schema';
|
import { CreateSLOInput, MetricCustomIndicator } from '@kbn/slo-schema';
|
||||||
import { FormState, UseFormGetFieldState, UseFormGetValues, UseFormWatch } from 'react-hook-form';
|
import { FormState, UseFormGetFieldState, UseFormGetValues, UseFormWatch } from 'react-hook-form';
|
||||||
import { isObject } from 'lodash';
|
import { isObject } from 'lodash';
|
||||||
|
|
||||||
|
@ -22,9 +22,7 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
|
||||||
switch (watch('indicator.type')) {
|
switch (watch('indicator.type')) {
|
||||||
case 'sli.metric.custom':
|
case 'sli.metric.custom':
|
||||||
const isGoodParamsValid = () => {
|
const isGoodParamsValid = () => {
|
||||||
const data = getValues(
|
const data = getValues('indicator.params.good') as MetricCustomIndicator['params']['good'];
|
||||||
'indicator.params.good'
|
|
||||||
) as MetricCustomIndicatorSchema['params']['good'];
|
|
||||||
const isEquationValid = !getFieldState('indicator.params.good.equation').invalid;
|
const isEquationValid = !getFieldState('indicator.params.good.equation').invalid;
|
||||||
const areMetricsValid =
|
const areMetricsValid =
|
||||||
isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field));
|
isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field));
|
||||||
|
@ -34,7 +32,7 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
|
||||||
const isTotalParamsValid = () => {
|
const isTotalParamsValid = () => {
|
||||||
const data = getValues(
|
const data = getValues(
|
||||||
'indicator.params.total'
|
'indicator.params.total'
|
||||||
) as MetricCustomIndicatorSchema['params']['total'];
|
) as MetricCustomIndicator['params']['total'];
|
||||||
const isEquationValid = !getFieldState('indicator.params.total.equation').invalid;
|
const isEquationValid = !getFieldState('indicator.params.total.equation').invalid;
|
||||||
const areMetricsValid =
|
const areMetricsValid =
|
||||||
isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field));
|
isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field));
|
||||||
|
|
|
@ -66,6 +66,12 @@ const mockKibana = () => {
|
||||||
application: {
|
application: {
|
||||||
navigateToUrl: mockNavigate,
|
navigateToUrl: mockNavigate,
|
||||||
},
|
},
|
||||||
|
charts: {
|
||||||
|
theme: {
|
||||||
|
useChartsTheme: () => {},
|
||||||
|
useChartsBaseTheme: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
dataViews: {
|
dataViews: {
|
||||||
find: jest.fn().mockReturnValue([]),
|
find: jest.fn().mockReturnValue([]),
|
||||||
|
|
|
@ -20,6 +20,7 @@ export class SLOIdConflict extends ObservabilityError {}
|
||||||
export class CompositeSLONotFound extends ObservabilityError {}
|
export class CompositeSLONotFound extends ObservabilityError {}
|
||||||
export class CompositeSLOIdConflict extends ObservabilityError {}
|
export class CompositeSLOIdConflict extends ObservabilityError {}
|
||||||
|
|
||||||
|
export class InvalidQueryError extends ObservabilityError {}
|
||||||
export class InternalQueryError extends ObservabilityError {}
|
export class InternalQueryError extends ObservabilityError {}
|
||||||
export class NotSupportedError extends ObservabilityError {}
|
export class NotSupportedError extends ObservabilityError {}
|
||||||
export class IllegalArgumentError extends ObservabilityError {}
|
export class IllegalArgumentError extends ObservabilityError {}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
deleteSLOParamsSchema,
|
deleteSLOParamsSchema,
|
||||||
fetchHistoricalSummaryParamsSchema,
|
fetchHistoricalSummaryParamsSchema,
|
||||||
findSLOParamsSchema,
|
findSLOParamsSchema,
|
||||||
|
getPreviewDataParamsSchema,
|
||||||
getSLODiagnosisParamsSchema,
|
getSLODiagnosisParamsSchema,
|
||||||
getSLOParamsSchema,
|
getSLOParamsSchema,
|
||||||
manageSLOParamsSchema,
|
manageSLOParamsSchema,
|
||||||
|
@ -41,6 +42,7 @@ import type { IndicatorTypes } from '../../domain/models';
|
||||||
import type { ObservabilityRequestHandlerContext } from '../../types';
|
import type { ObservabilityRequestHandlerContext } from '../../types';
|
||||||
import { ManageSLO } from '../../services/slo/manage_slo';
|
import { ManageSLO } from '../../services/slo/manage_slo';
|
||||||
import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis';
|
import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis';
|
||||||
|
import { GetPreviewData } from '../../services/slo/get_preview_data';
|
||||||
|
|
||||||
const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
|
const transformGenerators: Record<IndicatorTypes, TransformGenerator> = {
|
||||||
'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(),
|
'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(),
|
||||||
|
@ -303,6 +305,25 @@ const getSloDiagnosisRoute = createObservabilityServerRoute({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getPreviewData = createObservabilityServerRoute({
|
||||||
|
endpoint: 'POST /internal/observability/slos/_preview',
|
||||||
|
options: {
|
||||||
|
tags: ['access:slo_read'],
|
||||||
|
},
|
||||||
|
params: getPreviewDataParamsSchema,
|
||||||
|
handler: async ({ context, params }) => {
|
||||||
|
const hasCorrectLicense = await isLicenseAtLeastPlatinum(context);
|
||||||
|
|
||||||
|
if (!hasCorrectLicense) {
|
||||||
|
throw badRequest('Platinum license or higher is needed to make use of this feature.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||||
|
const service = new GetPreviewData(esClient);
|
||||||
|
return await service.execute(params.body);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const sloRouteRepository = {
|
export const sloRouteRepository = {
|
||||||
...createSLORoute,
|
...createSLORoute,
|
||||||
...deleteSLORoute,
|
...deleteSLORoute,
|
||||||
|
@ -314,4 +335,5 @@ export const sloRouteRepository = {
|
||||||
...updateSLORoute,
|
...updateSLORoute,
|
||||||
...getDiagnosisRoute,
|
...getDiagnosisRoute,
|
||||||
...getSloDiagnosisRoute,
|
...getSloDiagnosisRoute,
|
||||||
|
...getPreviewData,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||||
|
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||||
|
import { GetPreviewDataParams, GetPreviewDataResponse } from '@kbn/slo-schema';
|
||||||
|
import { computeSLI } from '../../domain/services';
|
||||||
|
import { InvalidQueryError } from '../../errors';
|
||||||
|
|
||||||
|
export class GetPreviewData {
|
||||||
|
constructor(private esClient: ElasticsearchClient) {}
|
||||||
|
|
||||||
|
public async execute(params: GetPreviewDataParams): Promise<GetPreviewDataResponse> {
|
||||||
|
switch (params.indicator.type) {
|
||||||
|
case 'sli.kql.custom':
|
||||||
|
const filterQuery = getElastichsearchQueryOrThrow(params.indicator.params.filter);
|
||||||
|
const goodQuery = getElastichsearchQueryOrThrow(params.indicator.params.good);
|
||||||
|
const totalQuery = getElastichsearchQueryOrThrow(params.indicator.params.total);
|
||||||
|
const timestampField = params.indicator.params.timestampField;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.esClient.search({
|
||||||
|
index: params.indicator.params.index,
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [{ range: { [timestampField]: { gte: 'now-60m' } } }, filterQuery],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aggs: {
|
||||||
|
perMinute: {
|
||||||
|
date_histogram: {
|
||||||
|
field: timestampField,
|
||||||
|
fixed_interval: '1m',
|
||||||
|
},
|
||||||
|
aggs: {
|
||||||
|
good: { filter: goodQuery },
|
||||||
|
total: { filter: totalQuery },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore buckets is not improperly typed
|
||||||
|
return result.aggregations?.perMinute.buckets.map((bucket) => ({
|
||||||
|
date: bucket.key_as_string,
|
||||||
|
sliValue: computeSLI(bucket.good.doc_count, bucket.total.doc_count),
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
throw new InvalidQueryError(`Invalid ES query`);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElastichsearchQueryOrThrow(kuery: string) {
|
||||||
|
try {
|
||||||
|
return toElasticsearchQuery(fromKueryExpression(kuery));
|
||||||
|
} catch (err) {
|
||||||
|
throw new InvalidQueryError(`Invalid kuery: ${kuery}`);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue