From 43413ea88a12cda5aeeb6916debb078ce741f2ba Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Mon, 18 Dec 2023 09:55:20 -0500 Subject: [PATCH] feat(slo): show cardinality info on group by field (#173400) --- .../slo/use_fetch_group_by_cardinality.ts | 69 +++++++++++++++++++ .../slo/use_fetch_index_pattern_fields.ts | 10 +-- .../apm_availability_indicator_type_form.tsx | 21 +++++- .../apm_latency_indicator_type_form.tsx | 29 +++++++- .../common/index_field_selector.tsx | 4 +- .../custom_kql_indicator_type_form.tsx | 22 +++++- .../custom_metric/custom_metric_type_form.tsx | 21 ++++++ .../custom_metric/metric_indicator.tsx | 4 +- .../histogram/histogram_indicator.tsx | 4 +- .../histogram_indicator_type_form.tsx | 20 ++++++ .../timeslice_metric/metric_indicator.tsx | 4 +- .../timeslice_metric/metric_input.tsx | 4 +- .../timeslice_metric_indicator.tsx | 21 ++++++ .../pages/slo_edit/helpers/create_options.ts | 4 +- 14 files changed, 214 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/observability/public/hooks/slo/use_fetch_group_by_cardinality.ts diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_group_by_cardinality.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_group_by_cardinality.ts new file mode 100644 index 0000000000000..928eb7e92482d --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_group_by_cardinality.ts @@ -0,0 +1,69 @@ +/* + * 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 { ALL_VALUE } from '@kbn/slo-schema'; +import { useQuery } from '@tanstack/react-query'; +import { lastValueFrom } from 'rxjs'; +import { useKibana } from '../../utils/kibana_react'; + +export interface UseFetchIndexPatternFieldsResponse { + isLoading: boolean; + isSuccess: boolean; + isError: boolean; + data?: { cardinality: number; isHighCardinality: boolean }; +} + +const HIGH_CARDINALITY_THRESHOLD = 1000; + +export function useFetchGroupByCardinality( + indexPattern: string, + timestampField: string, + groupBy: string +): UseFetchIndexPatternFieldsResponse { + const { data: dataService } = useKibana().services; + + const { isLoading, isError, isSuccess, data } = useQuery({ + queryKey: ['fetchGroupByCardinality', indexPattern, timestampField, groupBy], + queryFn: async ({ signal }) => { + try { + const result = await lastValueFrom( + dataService.search.search({ + params: { + index: indexPattern, + body: { + query: { + bool: { + filter: [{ range: { [timestampField]: { gte: 'now-24h' } } }], + }, + }, + aggs: { + groupByCardinality: { + cardinality: { + field: groupBy, + }, + }, + }, + }, + }, + }) + ); + + // @ts-expect-error Property 'value' does not exist on type 'AggregationsAggregate' + const cardinality = result.rawResponse?.aggregations?.groupByCardinality?.value ?? 0; + return { cardinality, isHighCardinality: cardinality > HIGH_CARDINALITY_THRESHOLD }; + } catch (error) { + throw new Error(`Something went wrong. Error: ${error}`); + } + }, + retry: false, + refetchOnWindowFocus: false, + enabled: + Boolean(indexPattern) && Boolean(timestampField) && Boolean(groupBy) && groupBy !== ALL_VALUE, + }); + + return { isLoading, isError, isSuccess, data }; +} diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts index 3363d501fae22..5aedb92219da6 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { FieldSpec } from '@kbn/data-views-plugin/common'; import { useQuery } from '@tanstack/react-query'; import { useKibana } from '../../utils/kibana_react'; @@ -12,14 +13,7 @@ export interface UseFetchIndexPatternFieldsResponse { isLoading: boolean; isSuccess: boolean; isError: boolean; - data: Field[] | undefined; -} - -export interface Field { - name: string; - type: string; - aggregatable: boolean; - searchable: boolean; + data: FieldSpec[] | undefined; } export function useFetchIndexPatternFields( diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx index c877d1de173c1..78afcc71cae58 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ALL_VALUE } from '@kbn/slo-schema/src/schema/common'; import React, { useEffect } from 'react'; import { useFormContext } from 'react-hook-form'; +import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality'; import { useFetchApmIndex } from '../../../../hooks/slo/use_fetch_apm_indices'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; @@ -27,9 +28,14 @@ export function ApmAvailabilityIndicatorTypeForm() { setValue('indicator.params.index', apmIndex); } }, [setValue, apmIndex]); + const timestampField = watch('indicator.params.timestampField'); + const groupByField = watch('groupBy'); const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = useFetchIndexPatternFields(apmIndex); + + const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } = + useFetchGroupByCardinality(apmIndex, timestampField, groupByField); const groupByFields = indexFields.filter((field) => field.aggregatable); return ( @@ -158,6 +164,19 @@ export function ApmAvailabilityIndicatorTypeForm() { isDisabled={!apmIndex} /> + {!isGroupByCardinalityLoading && !!groupByCardinality && ( + + )} + ); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx index b75c9fd8d110c..dcb4be69d272d 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/apm_latency/apm_latency_indicator_type_form.tsx @@ -5,11 +5,19 @@ * 2.0. */ -import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui'; +import { + EuiCallOut, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ALL_VALUE } from '@kbn/slo-schema/src/schema/common'; import React, { useEffect } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality'; import { useFetchApmIndex } from '../../../../hooks/slo/use_fetch_apm_indices'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; @@ -28,8 +36,14 @@ export function ApmLatencyIndicatorTypeForm() { } }, [setValue, apmIndex]); + const timestampField = watch('indicator.params.timestampField'); + const groupByField = watch('groupBy'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = useFetchIndexPatternFields(apmIndex); + + const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } = + useFetchGroupByCardinality(apmIndex, timestampField, groupByField); const groupByFields = indexFields.filter((field) => field.aggregatable); return ( @@ -201,6 +215,19 @@ export function ApmLatencyIndicatorTypeForm() { isDisabled={!apmIndex} /> + {!isGroupByCardinalityLoading && !!groupByCardinality && ( + + )} + ); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/index_field_selector.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/index_field_selector.tsx index 367ff255edde0..0a07515a8915f 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/common/index_field_selector.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/common/index_field_selector.tsx @@ -6,14 +6,14 @@ */ import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; import React, { useEffect, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { createOptionsFromFields, Option } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; interface Props { - indexFields: Field[]; + indexFields: FieldSpec[]; name: 'groupBy' | 'indicator.params.timestampField'; label: React.ReactNode | string; placeholder: string; diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx index 33b8a2eb6def3..42130cf204df6 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ALL_VALUE } from '@kbn/slo-schema/src/schema/common'; import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; @@ -20,11 +21,17 @@ import { IndexSelection } from '../custom_common/index_selection'; export function CustomKqlIndicatorTypeForm() { const { watch } = useFormContext(); const index = watch('indicator.params.index'); + const timestampField = watch('indicator.params.timestampField'); + const groupByField = watch('groupBy'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = useFetchIndexPatternFields(index); const timestampFields = indexFields.filter((field) => field.type === 'date'); const groupByFields = indexFields.filter((field) => field.aggregatable); + const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } = + useFetchGroupByCardinality(index, timestampField, groupByField); + return ( @@ -158,6 +165,19 @@ export function CustomKqlIndicatorTypeForm() { isDisabled={!index} /> + {!isGroupByCardinalityLoading && !!groupByCardinality && ( + + )} + ); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx index c7a180d59cfbe..2db17479ffd2d 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx @@ -6,6 +6,7 @@ */ import { + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, @@ -18,6 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { ALL_VALUE } from '@kbn/slo-schema/src/schema/common'; import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; @@ -33,8 +35,14 @@ const SUPPORTED_METRIC_FIELD_TYPES = ['number', 'histogram']; export function CustomMetricIndicatorTypeForm() { const { watch } = useFormContext(); const index = watch('indicator.params.index'); + const timestampField = watch('indicator.params.timestampField'); + const groupByField = watch('groupBy'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = useFetchIndexPatternFields(index); + const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } = + useFetchGroupByCardinality(index, timestampField, groupByField); + const timestampFields = indexFields.filter((field) => field.type === 'date'); const groupByFields = indexFields.filter((field) => field.aggregatable); const metricFields = indexFields.filter((field) => @@ -175,6 +183,19 @@ export function CustomMetricIndicatorTypeForm() { isDisabled={!index} /> + {!isGroupByCardinalityLoading && !!groupByCardinality && ( + + )} + diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx index 262e6e6d2249a..3e077ab2280a6 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx @@ -16,12 +16,12 @@ import { EuiIconTip, EuiSpacer, } from '@elastic/eui'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { first, range, xor } from 'lodash'; import React, { useEffect, useState } from 'react'; import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; -import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { aggValueToLabel, CUSTOM_METRIC_AGGREGATION_OPTIONS, @@ -32,7 +32,7 @@ import { QueryBuilder } from '../common/query_builder'; interface MetricIndicatorProps { type: 'good' | 'total'; - metricFields: Field[]; + metricFields: FieldSpec[]; isLoadingIndex: boolean; } diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx index 1362674828d74..4efcc1bfb5b8b 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator.tsx @@ -15,17 +15,17 @@ import { EuiIconTip, EuiSpacer, } from '@elastic/eui'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import React, { Fragment, useEffect, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { createOptionsFromFields, Option } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; import { QueryBuilder } from '../common/query_builder'; interface HistogramIndicatorProps { type: 'good' | 'total'; - histogramFields: Field[]; + histogramFields: FieldSpec[]; isLoadingIndex: boolean; } diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx index 9f8831fa9dfab..992547dc2802f 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/histogram/histogram_indicator_type_form.tsx @@ -6,6 +6,7 @@ */ import { + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, @@ -18,6 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { ALL_VALUE } from '@kbn/slo-schema/src/schema/common'; import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; @@ -29,9 +31,14 @@ import { HistogramIndicator } from './histogram_indicator'; export function HistogramIndicatorTypeForm() { const { watch } = useFormContext(); const index = watch('indicator.params.index'); + const timestampField = watch('indicator.params.timestampField'); + const groupByField = watch('groupBy'); const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = useFetchIndexPatternFields(index); + const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } = + useFetchGroupByCardinality(index, timestampField, groupByField); + const histogramFields = indexFields.filter((field) => field.type === 'histogram'); const timestampFields = indexFields.filter((field) => field.type === 'date'); const groupByFields = indexFields.filter((field) => field.aggregatable); @@ -162,6 +169,19 @@ export function HistogramIndicatorTypeForm() { isDisabled={!index} /> + {!isGroupByCardinalityLoading && !!groupByCardinality && ( + + )} + diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_indicator.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_indicator.tsx index 14cce99bb5d86..b8105814b852e 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_indicator.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_indicator.tsx @@ -17,18 +17,18 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { first, range, xor } from 'lodash'; import React from 'react'; import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; -import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { COMPARATOR_OPTIONS } from '../../constants'; import { CreateSLOForm } from '../../types'; import { MetricInput } from './metric_input'; interface MetricIndicatorProps { - indexFields: Field[]; + indexFields: FieldSpec[]; isLoadingIndex: boolean; } diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx index 95cc277556020..22cbc42366baa 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx @@ -12,10 +12,10 @@ import { EuiFormRow, EuiIconTip, } from '@elastic/eui'; +import { FieldSpec } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { AGGREGATION_OPTIONS, aggValueToLabel } from '../../helpers/aggregation_options'; import { createOptionsFromFields, Option } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; @@ -55,7 +55,7 @@ interface MetricInputProps { metricIndex: number; indexPattern: string; isLoadingIndex: boolean; - indexFields: Field[]; + indexFields: FieldSpec[]; } export function MetricInput({ diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx index 2fde2468d78ec..1c01219ffe1a2 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx @@ -6,6 +6,7 @@ */ import { + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, @@ -19,6 +20,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import { ALL_VALUE } from '@kbn/slo-schema/src/schema/common'; +import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality'; import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; @@ -34,8 +36,14 @@ export { NEW_TIMESLICE_METRIC } from './metric_indicator'; export function TimesliceMetricIndicatorTypeForm() { const { watch } = useFormContext(); const index = watch('indicator.params.index'); + const timestampField = watch('indicator.params.timestampField'); + const groupByField = watch('groupBy'); + const { isLoading: isIndexFieldsLoading, data: indexFields = [] } = useFetchIndexPatternFields(index); + const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } = + useFetchGroupByCardinality(index, timestampField, groupByField); + const timestampFields = indexFields.filter((field) => field.type === 'date'); const groupByFields = indexFields.filter((field) => field.aggregatable); const { uiSettings } = useKibana().services; @@ -152,6 +160,19 @@ export function TimesliceMetricIndicatorTypeForm() { isDisabled={!index} /> + {!isGroupByCardinalityLoading && !!groupByCardinality && ( + + )} + boolean ): Option[] { const options = fields