From f3fdb0f39840abeb917dd2f796e703e50985e674 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 7 Jun 2024 14:17:21 +0200 Subject: [PATCH] [SLO Form] Use saved Data view id , handle runtime mappings (#176662) ## Summary Fixes https://github.com/elastic/kibana/issues/173771 Use saved data view id instead of index pattern where it's available. Inject runtime mappings from the dataview into transform. - [ ] Go to Discover and add a runtime field to the data view (this is only available in Discover) - [ ] Make sure filtering works based on the data view We are not supporting "scripted fields" from the Index Management DataView editor. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../kbn-slo-schema/src/schema/indicators.ts | 9 +- .../observability/server/plugin.ts | 3 + .../server/routes/register_routes.ts | 2 + .../indicator_properties_custom_kql.yaml | 5 + .../indicator_properties_custom_metric.yaml | 5 + .../indicator_properties_histogram.yaml | 5 + ...indicator_properties_timeslice_metric.yaml | 5 + .../observability_solution/slo/kibana.jsonc | 1 + .../slo/public/hooks/use_create_data_view.ts | 39 ++- .../pages/slo_details/slo_details.test.tsx | 6 + .../apm_availability_indicator_type_form.tsx | 5 +- .../apm_latency_indicator_type_form.tsx | 6 +- .../components/common/query_builder.tsx | 11 +- .../components/common/query_search_bar.tsx | 6 +- .../components/common/runtime_field_used.tsx | 52 ++++ .../custom_common/index_selection.tsx | 121 ++++---- .../custom_common/use_adhoc_data_views.ts | 75 +++++ .../custom_kql_indicator_type_form.tsx | 12 +- .../custom_metric/custom_metric_type_form.tsx | 8 +- .../custom_metric/metric_indicator.tsx | 262 +++++++++--------- .../histogram/histogram_indicator.tsx | 8 +- .../histogram_indicator_type_form.tsx | 6 +- ...etics_availability_indicator_type_form.tsx | 10 +- .../timeslice_metric/metric_indicator.tsx | 37 ++- .../timeslice_metric/metric_input.tsx | 29 -- .../timeslice_metric_indicator.tsx | 7 +- .../slo_edit/hooks/use_find_runtime_usage.ts | 50 ++++ .../public/pages/slo_edit/slo_edit.test.tsx | 22 +- .../slo/public/pages/slos/slos.test.tsx | 4 + .../slo/server/plugin.ts | 6 + .../slo/server/routes/register_routes.ts | 2 + .../slo/server/routes/slo/route.ts | 45 ++- .../slo/server/services/create_slo.ts | 8 +- .../slo/server/services/get_preview_data.ts | 40 ++- .../services/summay_transform_manager.ts | 4 +- .../apm_transaction_duration.test.ts | 46 +-- .../apm_transaction_duration.ts | 23 +- .../apm_transaction_error_rate.test.ts | 34 +-- .../apm_transaction_error_rate.ts | 24 +- .../services/transform_generators/common.ts | 5 +- .../transform_generators/histogram.test.ts | 46 +-- .../transform_generators/histogram.ts | 29 +- .../transform_generators/kql_custom.test.ts | 40 +-- .../transform_generators/kql_custom.ts | 26 +- .../metric_custom.test.ts | 60 ++-- .../transform_generators/metric_custom.ts | 28 +- .../synthetics_availability.test.ts | 49 ++-- .../synthetics_availability.ts | 23 +- .../timeslice_metric.test.ts | 41 +-- .../transform_generators/timeslice_metric.ts | 23 +- .../transform_generator.ts | 27 +- .../server/services/transform_manager.test.ts | 43 ++- .../slo/server/services/transform_manager.ts | 16 +- 53 files changed, 1017 insertions(+), 482 deletions(-) create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/runtime_field_used.tsx create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_find_runtime_usage.ts diff --git a/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts b/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts index f24f85e4f3ac45..6c3149c3c35e1e 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts @@ -25,9 +25,9 @@ const filtersSchema = t.array( isMultiIndex: t.boolean, type: t.string, key: t.string, + field: t.string, params: t.any, value: t.string, - field: t.string, }), query: t.record(t.string, t.any), }) @@ -54,6 +54,7 @@ const apmTransactionDurationIndicatorSchema = t.type({ }), t.partial({ filter: querySchema, + dataViewId: t.string, }), ]), }); @@ -71,6 +72,7 @@ const apmTransactionErrorRateIndicatorSchema = t.type({ }), t.partial({ filter: querySchema, + dataViewId: t.string, }), ]), }); @@ -87,6 +89,7 @@ const kqlCustomIndicatorSchema = t.type({ }), t.partial({ filter: querySchema, + dataViewId: t.string, }), ]), }); @@ -164,6 +167,7 @@ const timesliceMetricIndicatorSchema = t.type({ }), t.partial({ filter: querySchema, + dataViewId: t.string, }), ]), }); @@ -205,6 +209,7 @@ const metricCustomIndicatorSchema = t.type({ }), t.partial({ filter: querySchema, + dataViewId: t.string, }), ]), }); @@ -250,6 +255,7 @@ const histogramIndicatorSchema = t.type({ }), t.partial({ filter: querySchema, + dataViewId: t.string, }), ]), }); @@ -270,6 +276,7 @@ const syntheticsAvailabilityIndicatorSchema = t.type({ tags: t.array(syntheticsParamSchema), projects: t.array(syntheticsParamSchema), filter: querySchema, + dataViewId: t.string, }), ]), }); diff --git a/x-pack/plugins/observability_solution/observability/server/plugin.ts b/x-pack/plugins/observability_solution/observability/server/plugin.ts index fad3db3f5fd109..dad2dd7ee572a8 100644 --- a/x-pack/plugins/observability_solution/observability/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/server/plugin.ts @@ -36,6 +36,7 @@ import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/serve import { SharePluginSetup } from '@kbn/share-plugin/server'; import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { ObservabilityConfig } from '.'; import { casesFeatureId, observabilityFeatureId } from '../common'; import { @@ -71,6 +72,7 @@ interface PluginSetup { interface PluginStart { alerting: PluginStartContract; spaces?: SpacesPluginStart; + dataViews: DataViewsServerPluginStart; } const o11yRuleTypes = [ @@ -294,6 +296,7 @@ export class ObservabilityPlugin implements Plugin { ...plugins, core, }, + dataViews: pluginStart.dataViews, spaces: pluginStart.spaces, ruleDataService, assistant: { diff --git a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts index 373e91d89a1c3a..f0f41a70bec811 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts @@ -17,6 +17,7 @@ import { import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import axios from 'axios'; import * as t from 'io-ts'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { ObservabilityConfig } from '..'; import { getHTTPResponseCode, ObservabilityError } from '../errors'; import { AlertDetailsContextualInsightsService } from '../services'; @@ -35,6 +36,7 @@ export interface RegisterRoutesDependencies { pluginsSetup: { core: CoreSetup; }; + dataViews: DataViewsServerPluginStart; spaces?: SpacesPluginStart; ruleDataService: RuleDataPluginService; assistant: { diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_kql.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_kql.yaml index 429847630a7d21..8f6b9accbfafff 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_kql.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_kql.yaml @@ -19,6 +19,11 @@ properties: description: The index or index pattern to use type: string example: my-service-* + dataViewId: + description: The kibana data view id to use, primarily used to include data view runtime mappings. + Make sure to save SLO again if you add/update run time fields to the data view and if those fields are being used in slo queries. + type: string + example: 03b80ab3-003d-498b-881c-3beedbaf1162 filter: description: the KQL query to filter the documents with. $ref: "kql_with_filters.yaml" diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml index d97dc32fe0d11c..1d9a0ed8279139 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml @@ -19,6 +19,11 @@ properties: description: The index or index pattern to use type: string example: my-service-* + dataViewId: + description: The kibana data view id to use, primarily used to include data view runtime mappings. + Make sure to save SLO again if you add/update run time fields to the data view and if those fields are being used in slo queries. + type: string + example: 03b80ab3-003d-498b-881c-3beedbaf1162 filter: description: the KQL query to filter the documents with. type: string diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_histogram.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_histogram.yaml index dc39a3599a7b2f..9dbaa87e838a59 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_histogram.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_histogram.yaml @@ -19,6 +19,11 @@ properties: description: The index or index pattern to use type: string example: my-service-* + dataViewId: + description: The kibana data view id to use, primarily used to include data view runtime mappings. + Make sure to save SLO again if you add/update run time fields to the data view and if those fields are being used in slo queries. + type: string + example: 03b80ab3-003d-498b-881c-3beedbaf1162 filter: description: the KQL query to filter the documents with. type: string diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_timeslice_metric.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_timeslice_metric.yaml index 712420f059fdd3..43d399c733e022 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_timeslice_metric.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/indicator_properties_timeslice_metric.yaml @@ -18,6 +18,11 @@ properties: description: The index or index pattern to use type: string example: my-service-* + dataViewId: + description: The kibana data view id to use, primarily used to include data view runtime mappings. + Make sure to save SLO again if you add/update run time fields to the data view and if those fields are being used in slo queries. + type: string + example: 03b80ab3-003d-498b-881c-3beedbaf1162 filter: description: the KQL query to filter the documents with. type: string diff --git a/x-pack/plugins/observability_solution/slo/kibana.jsonc b/x-pack/plugins/observability_solution/slo/kibana.jsonc index 052eaaae9e1d6c..c03e89b691255b 100644 --- a/x-pack/plugins/observability_solution/slo/kibana.jsonc +++ b/x-pack/plugins/observability_solution/slo/kibana.jsonc @@ -22,6 +22,7 @@ "dataViews", "lens", "dataViewEditor", + "dataViewFieldEditor", "fieldFormats", "observability", "observabilityShared", diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts index 114d61b69a2b25..781145885df5f1 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts @@ -5,39 +5,36 @@ * 2.0. */ -import { useEffect, useState } from 'react'; -import { DataView } from '@kbn/data-views-plugin/common'; +import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '../utils/kibana_react'; interface UseCreateDataViewProps { indexPatternString?: string; + dataViewId?: string; } -export function useCreateDataView({ indexPatternString }: UseCreateDataViewProps) { +export function useCreateDataView({ indexPatternString, dataViewId }: UseCreateDataViewProps) { const { dataViews } = useKibana().services; - const [stateDataView, setStateDataView] = useState(); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - const createDataView = () => - dataViews.create({ + const { data: dataView, loading } = useFetcher(async () => { + if (dataViewId) { + try { + return await dataViews.get(dataViewId); + } catch (e) { + return dataViews.create({ + id: `${indexPatternString}-id`, + title: indexPatternString, + allowNoIndex: true, + }); + } + } else if (indexPatternString) { + return dataViews.create({ id: `${indexPatternString}-id`, title: indexPatternString, allowNoIndex: true, }); - - if (indexPatternString) { - setIsLoading(true); - createDataView() - .then((value) => { - setStateDataView(value); - }) - .finally(() => { - setIsLoading(false); - }); } - }, [indexPatternString, dataViews]); + }, [dataViewId, dataViews, indexPatternString]); - return { dataView: stateDataView, loading: isLoading }; + return { dataView, loading: Boolean(loading) }; } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx index 842c30073ecb19..508f8295121d27 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx @@ -32,6 +32,7 @@ import { useKibana } from '../../utils/kibana_react'; import { render } from '../../utils/test_helper'; import { SloDetailsPage } from './slo_details'; import { TagsList, HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; +import { useCreateDataView } from '../../hooks/use_create_data_view'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -46,6 +47,7 @@ jest.mock('../../hooks/use_fetch_active_alerts'); jest.mock('../../hooks/use_fetch_slo_details'); jest.mock('../../hooks/use_fetch_historical_summary'); jest.mock('../../hooks/use_delete_slo'); +jest.mock('../../hooks/use_create_data_view'); jest.mock('../../hooks/use_delete_slo_instance'); const useKibanaMock = useKibana as jest.Mock; @@ -56,6 +58,7 @@ const useFetchActiveAlertsMock = useFetchActiveAlerts as jest.Mock; const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock; const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock; const useDeleteSloMock = useDeleteSlo as jest.Mock; +const useCreateDataViewsMock = useCreateDataView as jest.Mock; const useDeleteSloInstanceMock = useDeleteSloInstance as jest.Mock; const TagsListMock = TagsList as jest.Mock; TagsListMock.mockReturnValue(
Tags list
); @@ -132,6 +135,9 @@ describe('SLO Details Page', () => { jest.clearAllMocks(); mockKibana(); useCapabilitiesMock.mockReturnValue({ hasWriteCapabilities: true, hasReadCapabilities: true }); + useCreateDataViewsMock.mockReturnValue({ + dataView: { getName: () => 'dataview', getIndexPattern: () => '.dataview-index' }, + }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, data: historicalSummaryData, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx index ee08e073bcac9e..7424db8a448e64 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/apm_availability/apm_availability_indicator_type_form.tsx @@ -10,6 +10,7 @@ import { APMTransactionErrorRateIndicator } from '@kbn/slo-schema'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import { useFormContext } from 'react-hook-form'; +import { DATA_VIEW_FIELD } from '../custom_common/index_selection'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; import { GroupByField } from '../common/group_by_field'; import { useFetchApmIndex } from '../../../../hooks/use_fetch_apm_indices'; @@ -23,6 +24,7 @@ import { getGroupByCardinalityFilters } from '../apm_common/get_group_by_cardina export function ApmAvailabilityIndicatorTypeForm() { const { watch, setValue } = useFormContext>(); const { data: apmIndex } = useFetchApmIndex(); + const dataViewId = watch(DATA_VIEW_FIELD); const [ serviceName = '', @@ -53,6 +55,7 @@ export function ApmAvailabilityIndicatorTypeForm() { const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({ indexPatternString: apmIndex, + dataViewId, }); return ( @@ -129,7 +132,7 @@ export function ApmAvailabilityIndicatorTypeForm() { ; placeholder: string; @@ -25,10 +26,7 @@ export interface SearchBarProps { } export function QueryBuilder(props: SearchBarProps) { - const { indexPatternString, name } = props; - const { dataView } = useCreateDataView({ - indexPatternString, - }); + const { dataView, name } = props; const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const [range, setRange] = useState({ from: 'now-15m', to: 'now' }); @@ -67,6 +65,7 @@ export function QueryBuilder(props: SearchBarProps) { searchBarProps={props} /> )} + ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx index 49ddd17e65a4e2..d238aacf1df600 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx @@ -12,7 +12,6 @@ import { kqlQuerySchema, kqlWithFiltersSchema } from '@kbn/slo-schema'; import React, { memo } from 'react'; import styled from 'styled-components'; import { observabilityAppId } from '@kbn/observability-shared-plugin/common'; -import { useCreateDataView } from '../../../../hooks/use_create_data_view'; import { SearchBarProps } from './query_builder'; import { useKibana } from '../../../../utils/kibana_react'; import { CreateSLOForm } from '../../types'; @@ -23,7 +22,7 @@ export const QuerySearchBar = memo( isFlyoutOpen, name, label, - indexPatternString, + dataView, required, tooltip, dataTestSubj, @@ -36,9 +35,6 @@ export const QuerySearchBar = memo( setRange: (range: TimeRange) => void; }) => { const { SearchBar } = useKibana().services.unifiedSearch.ui; - const { dataView } = useCreateDataView({ - indexPatternString, - }); const { control } = useFormContext(); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/runtime_field_used.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/runtime_field_used.tsx new file mode 100644 index 00000000000000..a937a1577e0df4 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/runtime_field_used.tsx @@ -0,0 +1,52 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +import { FieldPath } from 'react-hook-form'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { useRunTimeFieldBeingUsed } from '../../hooks/use_find_runtime_usage'; +import { CreateSLOForm } from '../../types'; + +export function RunTimeFieldUsed({ + dataView, + name, +}: { + dataView?: DataView; + name: FieldPath; +}) { + const fieldNames = useRunTimeFieldBeingUsed(name, dataView); + + if (fieldNames.length === 0) { + return null; + } + + return ( + <> + + +

+ {fieldNames.join(', ')}, + }} + /> +

+
+ + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx index fe023cd8595490..526c955f2c6d83 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx @@ -8,103 +8,114 @@ import { EuiFormRow } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { DataViewPicker } from '@kbn/unified-search-plugin/public'; -import { useFetchDataViews } from '@kbn/observability-plugin/public'; +import { getDataViewPattern, useAdhocDataViews } from './use_adhoc_data_views'; +import { SloPublicPluginsStart } from '../../../..'; import { useKibana } from '../../../../utils/kibana_react'; import { CreateSLOForm } from '../../types'; -export function IndexSelection() { - const { control, getFieldState, setValue, watch } = useFormContext(); - const { dataViews: dataViewsService } = useKibana().services; +export const DATA_VIEW_FIELD = 'indicator.params.dataViewId'; +const INDEX_FIELD = 'indicator.params.index'; +const TIMESTAMP_FIELD = 'indicator.params.timestampField'; - const { isLoading: isDataViewsLoading, data: dataViews = [], refetch } = useFetchDataViews(); +export function IndexSelection({ selectedDataView }: { selectedDataView?: DataView }) { + const { control, getFieldState, setValue, watch } = useFormContext(); + const { dataViews: dataViewsService, dataViewFieldEditor } = useKibana().services; - const { dataViewEditor } = useKibana().services; + const { dataViewEditor } = useKibana().services; - const [adHocDataViews, setAdHocDataViews] = useState([]); + const currentIndexPattern = watch(INDEX_FIELD); + const currentDataViewId = watch(DATA_VIEW_FIELD); - const currentIndexPattern = watch('indicator.params.index'); + const { dataViewsList, isDataViewsLoading, adHocDataViews, setAdHocDataViews, refetch } = + useAdhocDataViews({ + currentIndexPattern, + }); useEffect(() => { - if (!isDataViewsLoading) { - const missingAdHocDataView = - dataViews.find((dataView) => dataView.title === currentIndexPattern) || - adHocDataViews.find((dataView) => dataView.getIndexPattern() === currentIndexPattern); - - if (!missingAdHocDataView && currentIndexPattern) { - async function loadMissingDataView() { - const dataView = await dataViewsService.create( - { - title: currentIndexPattern, - allowNoIndex: true, - }, - true - ); - if (dataView.getIndexPattern() === currentIndexPattern) { - setAdHocDataViews((prev) => [...prev, dataView]); - } - } - - loadMissingDataView(); - } + const indPatternId = getDataViewPattern({ + byPatten: currentIndexPattern, + dataViewsList, + adHocDataViews, + }); + if (!currentDataViewId && currentIndexPattern && !isDataViewsLoading && indPatternId) { + setValue(DATA_VIEW_FIELD, indPatternId); } - }, [adHocDataViews, currentIndexPattern, dataViews, dataViewsService, isDataViewsLoading]); - - const getDataViewPatternById = (id?: string) => { - return ( - dataViews.find((dataView) => dataView.id === id)?.title || - adHocDataViews.find((dataView) => dataView.id === id)?.getIndexPattern() - ); - }; - - const getDataViewIdByIndexPattern = (indexPattern: string) => { - return ( - dataViews.find((dataView) => dataView.title === indexPattern) || - adHocDataViews.find((dataView) => dataView.getIndexPattern() === indexPattern) - ); - }; + }, [ + adHocDataViews, + currentDataViewId, + currentIndexPattern, + dataViewsList, + isDataViewsLoading, + setValue, + ]); return ( - + ( { - field.onChange(getDataViewPatternById(newId)); + setValue( + INDEX_FIELD, + getDataViewPattern({ byId: newId, adHocDataViews, dataViewsList })! + ); + field.onChange(newId); dataViewsService.get(newId).then((dataView) => { if (dataView.timeFieldName) { - setValue('indicator.params.timestampField', dataView.timeFieldName); + setValue(TIMESTAMP_FIELD, dataView.timeFieldName); } }); }} - currentDataViewId={getDataViewIdByIndexPattern(field.value)?.id} + onAddField={ + currentDataViewId && selectedDataView + ? () => { + dataViewFieldEditor.openEditor({ + ctx: { + dataView: selectedDataView, + }, + onSave: () => {}, + }); + } + : undefined + } + currentDataViewId={ + field.value ?? + getDataViewPattern({ + byPatten: currentIndexPattern, + dataViewsList, + adHocDataViews, + }) + } onDataViewCreated={() => { dataViewEditor.openEditor({ allowAdHocDataView: true, onSave: (dataView: DataView) => { if (!dataView.isPersisted()) { setAdHocDataViews([...adHocDataViews, dataView]); - field.onChange(dataView.getIndexPattern()); + field.onChange(dataView.id); + setValue(INDEX_FIELD, dataView.getIndexPattern()); } else { refetch(); - field.onChange(dataView.getIndexPattern()); + field.onChange(dataView.id); + setValue(INDEX_FIELD, dataView.getIndexPattern()); } if (dataView.timeFieldName) { - setValue('indicator.params.timestampField', dataView.timeFieldName); + setValue(TIMESTAMP_FIELD, dataView.timeFieldName); } }, }); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts new file mode 100644 index 00000000000000..6c8288df6b9d98 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts @@ -0,0 +1,75 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { DataView, DataViewListItem } from '@kbn/data-views-plugin/common'; +import { useFetchDataViews } from '@kbn/observability-plugin/public'; +import { useKibana } from '../../../../utils/kibana_react'; + +export const getDataViewPattern = ({ + byId, + byPatten, + dataViewsList, + adHocDataViews, +}: { + byId?: string; + byPatten?: string; + dataViewsList: DataViewListItem[]; + adHocDataViews: DataView[]; +}) => { + const allDataViews = [ + ...(dataViewsList ?? []), + ...adHocDataViews.map((dv) => ({ id: dv.id, title: dv.getIndexPattern() })), + ]; + if (byId) { + return allDataViews.find((dv) => dv.id === byId)?.title; + } + if (byPatten) { + return allDataViews.find((dv) => dv.title === byPatten)?.id; + } +}; + +export const useAdhocDataViews = ({ currentIndexPattern }: { currentIndexPattern: string }) => { + const { isLoading: isDataViewsLoading, data: dataViewsList = [], refetch } = useFetchDataViews(); + const { dataViews: dataViewsService } = useKibana().services; + const [adHocDataViews, setAdHocDataViews] = useState([]); + + useEffect(() => { + if (!isDataViewsLoading) { + const missingDataView = getDataViewPattern({ + byPatten: currentIndexPattern, + dataViewsList, + adHocDataViews, + }); + + if (!missingDataView && currentIndexPattern) { + async function loadMissingDataView() { + const dataView = await dataViewsService.create( + { + title: currentIndexPattern, + allowNoIndex: true, + }, + true + ); + if (dataView.getIndexPattern() === currentIndexPattern) { + setAdHocDataViews((prev) => [...prev, dataView]); + } + } + + loadMissingDataView(); + } + } + }, [adHocDataViews, currentIndexPattern, dataViewsList, dataViewsService, isDataViewsLoading]); + + return { + adHocDataViews, + setAdHocDataViews, + dataViewsList, + isDataViewsLoading, + refetch, + }; +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx index b1dc9da27762f8..43f06480840868 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx @@ -15,14 +15,16 @@ import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; -import { IndexSelection } from '../custom_common/index_selection'; +import { DATA_VIEW_FIELD, IndexSelection } from '../custom_common/index_selection'; export function CustomKqlIndicatorTypeForm() { const { watch } = useFormContext(); const index = watch('indicator.params.index'); + const dataViewId = watch(DATA_VIEW_FIELD); const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({ indexPatternString: index, + dataViewId, }); const timestampFields = dataView?.fields?.filter((field) => field.type === 'date') ?? []; @@ -30,7 +32,7 @@ export function CustomKqlIndicatorTypeForm() { - + (); const index = watch('indicator.params.index'); + const dataViewId = watch(DATA_VIEW_FIELD); const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({ indexPatternString: index, + dataViewId, }); const timestampFields = dataView?.fields.filter((field) => field.type === 'date'); @@ -79,7 +81,7 @@ export function CustomMetricIndicatorTypeForm() { @@ -141,6 +144,7 @@ export function CustomMetricIndicatorTypeForm() { type="total" metricFields={metricFields ?? []} isLoadingIndex={isIndexFieldsLoading} + dataView={dataView} />
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx index de19f2f5146c9e..03939dce314b6c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_metric/metric_indicator.tsx @@ -13,10 +13,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiHorizontalRule, EuiIconTip, EuiSpacer, } from '@elastic/eui'; -import { FieldSpec } from '@kbn/data-views-plugin/common'; +import { DataView, FieldSpec } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { first, range, xor } from 'lodash'; @@ -34,6 +35,7 @@ interface MetricIndicatorProps { type: 'good' | 'total'; metricFields: FieldSpec[]; isLoadingIndex: boolean; + dataView?: DataView; } export const NEW_CUSTOM_METRIC = { name: 'A', aggregation: 'sum' as const, field: '' }; @@ -82,7 +84,12 @@ const equationTooltip = ( /> ); -export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIndicatorProps) { +export function MetricIndicator({ + type, + metricFields, + isLoadingIndex, + dataView, +}: MetricIndicatorProps) { const { control, watch, setValue, register, getFieldState } = useFormContext(); const [options, setOptions] = useState(createOptionsFromFields(metricFields)); const [aggregationOptions, setAggregationOptions] = useState(CUSTOM_METRIC_AGGREGATION_OPTIONS); @@ -125,96 +132,29 @@ export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIn return ( <> - {fields?.map((metric, index) => ( - - - - - {i18n.translate('xpack.slo.sloEdit.customMetric.aggregationLabel', { - defaultMessage: 'Aggregation', - })}{' '} - {metric.name} - - } - > - ( - { - if (selected.length) { - return field.onChange(selected[0].value); - } - field.onChange(''); - }} - selectedOptions={ - !!indexPattern && - !!field.value && - CUSTOM_METRIC_AGGREGATION_OPTIONS.some((agg) => agg.value === field.value) - ? [ - { - value: field.value, - label: aggValueToLabel(field.value), - }, - ] - : [] - } - onSearchChange={(searchValue: string) => { - setAggregationOptions( - CUSTOM_METRIC_AGGREGATION_OPTIONS.filter(({ value }) => - value.includes(searchValue) - ) - ); - }} - options={aggregationOptions} - /> - )} - /> - - - {watch(`indicator.params.${type}.metrics.${index}.aggregation`) !== 'doc_count' && ( + {fields?.map((metric, index, arr) => ( +
+ + - {metricLabel} {metric.name} {metricTooltip} + {i18n.translate('xpack.slo.sloEdit.customMetric.aggregationLabel', { + defaultMessage: 'Aggregation', + })}{' '} + {metric.name} } > ( metricField.name === field.value) + CUSTOM_METRIC_AGGREGATION_OPTIONS.some((agg) => agg.value === field.value) ? [ { value: field.value, - label: field.value, + label: aggValueToLabel(field.value), }, ] : [] } onSearchChange={(searchValue: string) => { - setOptions( - createOptionsFromFields(metricFields, ({ value }) => + setAggregationOptions( + CUSTOM_METRIC_AGGREGATION_OPTIONS.filter(({ value }) => value.includes(searchValue) ) ); }} - options={options} + options={aggregationOptions} /> )} /> - )} - - - } - /> - - - - - + {watch(`indicator.params.${type}.metrics.${index}.aggregation`) !== 'doc_count' && ( + + + {metricLabel} {metric.name} {metricTooltip} + + } + > + ( + { + if (selected.length) { + return field.onChange(selected[0].value); + } + field.onChange(''); + }} + selectedOptions={ + !!indexPattern && + !!field.value && + metricFields.some((metricField) => metricField.name === field.value) + ? [ + { + value: field.value, + label: field.value, + }, + ] + : [] + } + onSearchChange={(searchValue: string) => { + setOptions( + createOptionsFromFields(metricFields, ({ value }) => + value.includes(searchValue) + ) + ); + }} + options={options} + /> + )} + /> + + + )} + + + + + + } + /> + {index !== arr.length - 1 && } +
))} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/histogram/histogram_indicator.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/histogram/histogram_indicator.tsx index 3cbe4dcb798e47..009504e5e6979a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/histogram/histogram_indicator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/histogram/histogram_indicator.tsx @@ -15,7 +15,7 @@ import { EuiIconTip, EuiSpacer, } from '@elastic/eui'; -import { FieldSpec } from '@kbn/data-views-plugin/common'; +import { DataView, 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'; @@ -27,6 +27,7 @@ interface HistogramIndicatorProps { type: 'good' | 'total'; histogramFields: FieldSpec[]; isLoadingIndex: boolean; + dataView?: DataView; } const AGGREGATIONS = { @@ -50,7 +51,7 @@ const aggregationTooltip = ( @@ -94,6 +95,7 @@ export function HistogramIndicator({ type, histogramFields, isLoadingIndex, + dataView, }: HistogramIndicatorProps) { const { control, watch, getFieldState } = useFormContext(); const [options, setOptions] = useState(createOptionsFromFields(histogramFields)); @@ -282,7 +284,7 @@ export function HistogramIndicator({ (); const index = watch('indicator.params.index'); + const dataViewId = watch(DATA_VIEW_FIELD); const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({ indexPatternString: index, + dataViewId, }); const histogramFields = dataView?.fields.filter((field) => field.type === 'histogram'); @@ -73,7 +75,7 @@ export function HistogramIndicatorTypeForm() { >(); + const dataViewId = watch(DATA_VIEW_FIELD); const [monitorIds = [], projects = [], tags = [], index, globalFilters] = watch([ 'indicator.params.monitorIds', @@ -34,6 +37,11 @@ export function SyntheticsAvailabilityIndicatorTypeForm() { 'indicator.params.filter', ]); + const { dataView } = useCreateDataView({ + indexPatternString: index, + dataViewId, + }); + const [range, _] = useState({ from: moment().subtract(1, 'day').toDate(), to: new Date(), @@ -112,7 +120,7 @@ export function SyntheticsAvailabilityIndicatorTypeForm() { ); -export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicatorProps) { +export function MetricIndicator({ indexFields, isLoadingIndex, dataView }: MetricIndicatorProps) { const { control, watch, setValue, register, getFieldState } = useFormContext(); const { fields, append, remove } = useFieldArray({ @@ -112,7 +115,7 @@ export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicator return ( <> - {fields?.map((metric, index) => ( + {fields?.map((metric, index, arr) => ( @@ -140,7 +143,29 @@ export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicator /> - + + } + /> + {index !== arr.length - 1 && } ))} @@ -288,3 +313,7 @@ export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicator ); } + +const filterLabel = i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.filterLabel', { + defaultMessage: 'Filter', +}); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx index f4cb60d81daf27..ebb539b97dab2b 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/metric_input.tsx @@ -19,7 +19,6 @@ import { Controller, useFormContext } from 'react-hook-form'; import { AGGREGATION_OPTIONS, aggValueToLabel } from '../../helpers/aggregation_options'; import { createOptionsFromFields, Option } from '../../helpers/create_options'; import { CreateSLOForm } from '../../types'; -import { QueryBuilder } from '../common/query_builder'; const fieldLabel = i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.fieldLabel', { defaultMessage: 'Field', @@ -30,10 +29,6 @@ const aggregationLabel = i18n.translate( { defaultMessage: 'Aggregation' } ); -const filterLabel = i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.filterLabel', { - defaultMessage: 'Filter', -}); - const fieldTooltip = (
)} - - - } - /> - ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx index 8bb748ea075c7a..82736e4e24a251 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx @@ -24,7 +24,7 @@ import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; import { IndexFieldSelector } from '../common/index_field_selector'; import { QueryBuilder } from '../common/query_builder'; -import { IndexSelection } from '../custom_common/index_selection'; +import { DATA_VIEW_FIELD, IndexSelection } from '../custom_common/index_selection'; import { MetricIndicator } from './metric_indicator'; import { COMPARATOR_MAPPING } from '../../constants'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; @@ -34,9 +34,11 @@ export { NEW_TIMESLICE_METRIC } from './metric_indicator'; export function TimesliceMetricIndicatorTypeForm() { const { watch } = useFormContext(); const index = watch('indicator.params.index'); + const dataViewId = watch(DATA_VIEW_FIELD); const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({ indexPatternString: index, + dataViewId, }); const timestampFields = dataView?.fields.filter((field) => field.type === 'date'); @@ -78,7 +80,7 @@ export function TimesliceMetricIndicatorTypeForm() { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_find_runtime_usage.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_find_runtime_usage.ts new file mode 100644 index 00000000000000..c7406a5dcd8639 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_find_runtime_usage.ts @@ -0,0 +1,50 @@ +/* + * 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 { DataView } from '@kbn/data-views-plugin/common'; +import { QuerySchema, querySchema } from '@kbn/slo-schema'; +import { FieldPath, useFormContext } from 'react-hook-form'; +import { CreateSLOForm } from '../types'; +const isFieldBeingUsed = (fieldName: string, query?: QuerySchema) => { + if (!query) { + return false; + } + const checkKql = (kql: string) => { + const queryStr = kql.replace(/\s{2,}/g, ' ').trim(); + return queryStr.includes(`${fieldName} :`) || queryStr.includes(`${fieldName}:`); + }; + if (typeof query === 'string') { + return checkKql(query); + } else { + const kql = query.kqlQuery; + const inKql = kql && checkKql(kql); + const inFilter = + query.filters && + query.filters.some((filter) => { + return filter.meta.field === fieldName || filter.meta.key === fieldName; + }); + return inKql || inFilter; + } +}; + +export const useRunTimeFieldBeingUsed = ( + name: FieldPath, + dataView?: DataView +): string[] => { + const { watch } = useFormContext(); + const value = watch(name); + + if (!dataView || !value) { + return []; + } + const runTimeMappings = dataView.getRuntimeMappings(); + const filter = querySchema.is(value) ? value : undefined; + const fieldNames = Object.keys(runTimeMappings).filter((key) => { + return isFieldBeingUsed(key, filter); + }); + return fieldNames ?? []; +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx index 8925c22f820500..5f6916720902b0 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx @@ -21,6 +21,7 @@ import { useFetchApmSuggestions } from '../../hooks/use_fetch_apm_suggestions'; import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details'; import { useUpdateSlo } from '../../hooks/use_update_slo'; import { useCreateRule, useFetchDataViews } from '@kbn/observability-plugin/public'; +import { useCreateDataView } from '../../hooks/use_create_data_view'; import { useFetchIndices } from '../../hooks/use_fetch_indices'; import { useKibana } from '../../utils/kibana_react'; import { kibanaStartMock } from '../../utils/kibana_react.mock'; @@ -36,6 +37,7 @@ jest.mock('react-router-dom', () => ({ jest.mock('@kbn/observability-shared-plugin/public'); jest.mock('../../hooks/use_fetch_indices'); +jest.mock('../../hooks/use_create_data_view'); jest.mock('../../hooks/use_fetch_slo_details'); jest.mock('../../hooks/use_create_slo'); jest.mock('../../hooks/use_update_slo'); @@ -52,6 +54,7 @@ jest.mock('../../utils/kibana_react', () => ({ const useKibanaMock = useKibana as jest.Mock; const useFetchIndicesMock = useFetchIndices as jest.Mock; const useFetchDataViewsMock = useFetchDataViews as jest.Mock; +const useCreateDataViewsMock = useCreateDataView as jest.Mock; const useFetchSloMock = useFetchSloDetails as jest.Mock; const useCreateSloMock = useCreateSlo as jest.Mock; const useUpdateSloMock = useUpdateSlo as jest.Mock; @@ -90,6 +93,8 @@ const mockKibana = (license: ILicense | null = licenseMock) => { dataViews: { create: jest.fn().mockResolvedValue({ getIndexPattern: jest.fn().mockReturnValue('some-index'), + getRuntimeMappings: jest.fn().mockReturnValue({}), + id: 'some-data-view-id', }), }, docLinks: { @@ -156,8 +161,23 @@ describe('SLO Edit Page', () => { useFetchDataViewsMock.mockReturnValue({ isLoading: false, - data: [{ getName: () => 'dataview', getIndexPattern: () => '.dataview-index' }], + data: [ + { + getName: () => 'dataview', + getIndexPattern: () => '.dataview-index', + getRuntimeMappings: jest.fn().mockReturnValue({}), + }, + ], }); + + useCreateDataViewsMock.mockReturnValue({ + dataView: { + getName: () => 'dataview', + getIndexPattern: () => '.dataview-index', + getRuntimeMappings: jest.fn().mockReturnValue({}), + }, + }); + useFetchIndicesMock.mockReturnValue({ isLoading: false, data: ['some-index', 'index-2'], diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx index 007dd89382073b..1d6debec567b4a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx @@ -27,6 +27,7 @@ import { useKibana } from '../../utils/kibana_react'; import { render } from '../../utils/test_helper'; import { SlosPage } from './slos'; import { useGetSettings } from '../slo_settings/use_get_settings'; +import { useCreateDataView } from '../../hooks/use_create_data_view'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -43,6 +44,7 @@ jest.mock('../../hooks/use_delete_slo'); jest.mock('../../hooks/use_delete_slo_instance'); jest.mock('../../hooks/use_fetch_historical_summary'); jest.mock('../../hooks/use_capabilities'); +jest.mock('../../hooks/use_create_data_view'); const useGetSettingsMock = useGetSettings as jest.Mock; const useKibanaMock = useKibana as jest.Mock; @@ -53,6 +55,7 @@ const useDeleteSloMock = useDeleteSlo as jest.Mock; const useDeleteSloInstanceMock = useDeleteSloInstance as jest.Mock; const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock; const useCapabilitiesMock = useCapabilities as jest.Mock; +const useCreateDataViewMock = useCreateDataView as jest.Mock; const TagsListMock = TagsList as jest.Mock; TagsListMock.mockReturnValue(
Tags list
); @@ -67,6 +70,7 @@ const mockDeleteInstance = jest.fn(); useCreateSloMock.mockReturnValue({ mutate: mockCreateSlo }); useDeleteSloMock.mockReturnValue({ mutateAsync: mockDeleteSlo }); useDeleteSloInstanceMock.mockReturnValue({ mutateAsync: mockDeleteInstance }); +useCreateDataViewMock.mockReturnValue({}); const mockNavigate = jest.fn(); const mockAddSuccess = jest.fn(); diff --git a/x-pack/plugins/observability_solution/slo/server/plugin.ts b/x-pack/plugins/observability_solution/slo/server/plugin.ts index 7c3dd521a5d527..9c701a34774858 100644 --- a/x-pack/plugins/observability_solution/slo/server/plugin.ts +++ b/x-pack/plugins/observability_solution/slo/server/plugin.ts @@ -25,6 +25,7 @@ import { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { CloudSetup } from '@kbn/cloud-plugin/server'; import { SharePluginSetup } from '@kbn/share-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; @@ -60,6 +61,7 @@ export interface PluginStart { taskManager: TaskManagerStartContract; spaces?: SpacesPluginStart; ruleRegistry: RuleRegistryPluginStartContract; + dataViews: DataViewsServerPluginStart; } const sloRuleTypes = [SLO_BURN_RATE_RULE_TYPE_ID]; @@ -148,6 +150,10 @@ export class SloPlugin implements Plugin { ...plugins, core, }, + getDataViewsStart: async () => { + const [, pluginStart] = await core.getStartServices(); + return pluginStart.dataViews; + }, getSpacesStart: async () => { const [, pluginStart] = await core.getStartServices(); return pluginStart.spaces; diff --git a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts index e74b5abb811b98..8ddabede0d190d 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts @@ -21,6 +21,7 @@ import { import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import axios from 'axios'; import * as t from 'io-ts'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { SloConfig } from '..'; import { getHTTPResponseCode, ObservabilityError } from '../errors'; import { SloRequestHandlerContext } from '../types'; @@ -43,6 +44,7 @@ export interface RegisterRoutesDependencies { ruleDataService: RuleDataPluginService; getRulesClientWithRequest: (request: KibanaRequest) => Promise; getRacClientWithRequest: (request: KibanaRequest) => Promise; + getDataViewsStart: () => Promise; } export function registerRoutes({ config, repository, core, logger, dependencies }: RegisterRoutes) { diff --git a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts index 61c081b087e157..f5075abac0c0a1 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts @@ -101,17 +101,21 @@ const createSLORoute = createSloServerRoute({ await assertPlatinumLicense(context); const spaces = await dependencies.getSpacesStart(); + const dataViews = await dependencies.getDataViewsStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const basePath = dependencies.pluginsSetup.core.http.basePath; const soClient = (await context.core).savedObjects.client; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); + + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const transformManager = new DefaultTransformManager( transformGenerators, esClient, logger, - spaceId + spaceId, + dataViewsService ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), @@ -146,16 +150,19 @@ const inspectSLORoute = createSloServerRoute({ await assertPlatinumLicense(context); const spaces = await dependencies.getSpacesStart(); + const dataViews = await dependencies.getDataViewsStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; const basePath = dependencies.pluginsSetup.core.http.basePath; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const transformManager = new DefaultTransformManager( transformGenerators, esClient, logger, - spaceId + spaceId, + dataViewsService ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), @@ -189,17 +196,19 @@ const updateSLORoute = createSloServerRoute({ const spaces = await dependencies.getSpacesStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const dataViews = await dependencies.getDataViewsStart(); const basePath = dependencies.pluginsSetup.core.http.basePath; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; - + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const transformManager = new DefaultTransformManager( transformGenerators, esClient, logger, - spaceId + spaceId, + dataViewsService ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), @@ -235,17 +244,21 @@ const deleteSLORoute = createSloServerRoute({ const spaces = await dependencies.getSpacesStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const dataViews = await dependencies.getDataViewsStart(); const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; const rulesClient = await dependencies.getRulesClientWithRequest(request); + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); + const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const transformManager = new DefaultTransformManager( transformGenerators, esClient, logger, - spaceId + spaceId, + dataViewsService ); const summaryTransformManager = new DefaultSummaryTransformManager( @@ -302,16 +315,18 @@ const enableSLORoute = createSloServerRoute({ const spaces = await dependencies.getSpacesStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const dataViews = await dependencies.getDataViewsStart(); const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; - + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const transformManager = new DefaultTransformManager( transformGenerators, esClient, logger, - spaceId + spaceId, + dataViewsService ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), @@ -339,16 +354,18 @@ const disableSLORoute = createSloServerRoute({ const spaces = await dependencies.getSpacesStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const dataViews = await dependencies.getDataViewsStart(); const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; - + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const transformManager = new DefaultTransformManager( transformGenerators, esClient, logger, - spaceId + spaceId, + dataViewsService ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), @@ -375,17 +392,20 @@ const resetSLORoute = createSloServerRoute({ await assertPlatinumLicense(context); const spaces = await dependencies.getSpacesStart(); + const dataViews = await dependencies.getDataViewsStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const basePath = dependencies.pluginsSetup.core.http.basePath; + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const transformManager = new DefaultTransformManager( transformGenerators, esClient, logger, - spaceId + spaceId, + dataViewsService ); const summaryTransformManager = new DefaultSummaryTransformManager( new DefaultSummaryTransformGenerator(), @@ -629,9 +649,12 @@ const getPreviewData = createSloServerRoute({ const spaces = await dependencies.getSpacesStart(); const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const dataViews = await dependencies.getDataViewsStart(); const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const service = new GetPreviewData(esClient, spaceId); + const soClient = (await context.core).savedObjects.client; + const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); + const service = new GetPreviewData(esClient, spaceId, dataViewsService); return await service.execute(params.body); }, }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts index 60a5da93d9bf7d..e32969751fd936 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts @@ -89,7 +89,7 @@ export class CreateSLO { return this.toResponse(slo); } - public inspect(params: CreateSLOParams): { + public async inspect(params: CreateSLOParams): Promise<{ slo: CreateSLOParams; pipeline: Record; rollUpTransform: TransformPutTransformRequest; @@ -97,13 +97,13 @@ export class CreateSLO { temporaryDoc: Record; rollUpTransformCompositeQuery: string; summaryTransformCompositeQuery: string; - } { + }> { const slo = this.toSLO(params); validateSLO(slo); - const rollUpTransform = this.transformManager.inspect(slo); + const rollUpTransform = await this.transformManager.inspect(slo); const pipeline = getSLOSummaryPipelineTemplate(slo, this.spaceId, this.basePath); - const summaryTransform = this.summaryTransformManager.inspect(slo); + const summaryTransform = await this.summaryTransformManager.inspect(slo); const temporaryDoc = createTempSummaryDocument(slo, this.spaceId, this.basePath); return { diff --git a/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts b/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts index 06d740aca3508d..a18c4d035a0961 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts @@ -21,6 +21,7 @@ import { assertNever } from '@kbn/std'; import moment from 'moment'; import { ElasticsearchClient } from '@kbn/core/server'; import { estypes } from '@elastic/elasticsearch'; +import { DataView, DataViewsService } from '@kbn/data-views-plugin/common'; import { getElasticsearchQueryOrThrow } from './transform_generators'; import { buildParamValues } from './transform_generators/synthetics_availability'; @@ -46,7 +47,23 @@ interface Options { groupings?: Record; } export class GetPreviewData { - constructor(private esClient: ElasticsearchClient, private spaceId: string) {} + constructor( + private esClient: ElasticsearchClient, + private spaceId: string, + private dataViewService: DataViewsService + ) {} + + public async buildRuntimeMappings({ dataViewId }: { dataViewId?: string }) { + let dataView: DataView | undefined; + if (dataViewId) { + try { + dataView = await this.dataViewService.get(dataViewId); + } catch (e) { + // If the data view is not found, we will continue without it + } + } + return dataView?.getRuntimeMappings?.() ?? {}; + } private async getAPMTransactionDurationPreviewData( indicator: APMTransactionDurationIndicator, @@ -81,6 +98,9 @@ export class GetPreviewData { const result = await typedSearch(this.esClient, { index, + runtime_mappings: await this.buildRuntimeMappings({ + dataViewId: indicator.params.dataViewId, + }), size: 0, query: { bool: { @@ -177,6 +197,9 @@ export class GetPreviewData { const result = await this.esClient.search({ index, + runtime_mappings: await this.buildRuntimeMappings({ + dataViewId: indicator.params.dataViewId, + }), size: 0, query: { bool: { @@ -256,6 +279,9 @@ export class GetPreviewData { const result = await this.esClient.search({ index, + runtime_mappings: await this.buildRuntimeMappings({ + dataViewId: indicator.params.dataViewId, + }), size: 0, query: { bool: { @@ -321,6 +347,9 @@ export class GetPreviewData { const result = await this.esClient.search({ index, + runtime_mappings: await this.buildRuntimeMappings({ + dataViewId: indicator.params.dataViewId, + }), size: 0, query: { bool: { @@ -389,6 +418,9 @@ export class GetPreviewData { const result = await this.esClient.search({ index, + runtime_mappings: await this.buildRuntimeMappings({ + dataViewId: indicator.params.dataViewId, + }), size: 0, query: { bool: { @@ -440,6 +472,9 @@ export class GetPreviewData { const result = await this.esClient.search({ index, + runtime_mappings: await this.buildRuntimeMappings({ + dataViewId: indicator.params.dataViewId, + }), size: 0, query: { bool: { @@ -523,6 +558,9 @@ export class GetPreviewData { const result = await this.esClient.search({ index, + runtime_mappings: await this.buildRuntimeMappings({ + dataViewId: indicator.params.dataViewId, + }), size: 0, query: { bool: { diff --git a/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts b/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts index 27276f59330e88..5529591a81e204 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts @@ -23,7 +23,7 @@ export class DefaultSummaryTransformManager implements TransformManager { ) {} async install(slo: SLODefinition): Promise { - const transformParams = this.generator.generate(slo); + const transformParams = await this.generator.generate(slo); try { await retryTransientEsErrors(() => this.esClient.transform.putTransform(transformParams), { logger: this.logger, @@ -40,7 +40,7 @@ export class DefaultSummaryTransformManager implements TransformManager { return transformParams.transform_id; } - inspect(slo: SLODefinition): TransformPutTransformRequest { + async inspect(slo: SLODefinition): Promise { return this.generator.generate(slo); } diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts index 2ab6b264429673..b764b83ea93496 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts @@ -13,28 +13,30 @@ import { createSLOWithTimeslicesBudgetingMethod, } from '../fixtures/slo'; import { ApmTransactionDurationTransformGenerator } from './apm_transaction_duration'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; const generator = new ApmTransactionDurationTransformGenerator(); +const spaceId = 'custom-space'; describe('APM Transaction Duration Transform Generator', () => { - it('returns the expected transform params with every specified indicator params', () => { + it('returns the expected transform params with every specified indicator params', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createAPMTransactionDurationIndicator() }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); - it('returns the expected transform params for timeslices slo', () => { + it('returns the expected transform params for timeslices slo', async () => { const slo = createSLOWithTimeslicesBudgetingMethod({ id: 'irrelevant', indicator: createAPMTransactionDurationIndicator(), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); - it('returns the expected transform params for timeslices slo using a timesliceTarget = 0', () => { + it('returns the expected transform params for timeslices slo using a timesliceTarget = 0', async () => { const slo = createSLOWithTimeslicesBudgetingMethod({ id: 'irrelevant', indicator: createAPMTransactionDurationIndicator(), @@ -44,12 +46,12 @@ describe('APM Transaction Duration Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); - it("does not include the query filter when params are '*'", () => { + it("does not include the query filter when params are '*'", async () => { const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ environment: ALL_VALUE, @@ -58,36 +60,36 @@ describe('APM Transaction Duration Transform Generator', () => { transactionType: ALL_VALUE, }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); - it('uses the provided index params as source index', () => { + it('uses the provided index params as source index', async () => { const index = 'my-custom-apm-index*'; const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ index, }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.index).toEqual(index); }); - it('adds the custom kql filter to the query', () => { + it('adds the custom kql filter to the query', async () => { const filter = `"my.field" : "value" and ("foo" >= 12 or "bar" <= 100)`; const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ filter, }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); - it("groups by the 'service.name'", () => { + it("groups by the 'service.name'", async () => { const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ service: 'my-service', @@ -97,13 +99,13 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("groups by the 'service.environment'", () => { + it("groups by the 'service.environment'", async () => { const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ service: ALL_VALUE, @@ -113,13 +115,13 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("groups by the 'transaction.name'", () => { + it("groups by the 'transaction.name'", async () => { const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ service: ALL_VALUE, @@ -129,13 +131,13 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("groups by the 'transaction.type'", () => { + it("groups by the 'transaction.type'", async () => { const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ service: ALL_VALUE, @@ -145,13 +147,13 @@ describe('APM Transaction Duration Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("overrides the range filter when 'preventInitialBackfill' is true", () => { + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { const slo = createSLO({ indicator: createAPMTransactionDurationIndicator(), settings: { @@ -161,7 +163,7 @@ describe('APM Transaction Duration Transform Generator', () => { }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts index 6bbec476754cb7..afb37b4f0a1059 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts @@ -13,6 +13,7 @@ import { apmTransactionDurationIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; +import { DataViewsService } from '@kbn/data-views-plugin/common'; import { getElasticsearchQueryOrThrow, TransformGenerator } from '.'; import { getSLOTransformId, @@ -26,7 +27,11 @@ import { parseIndex } from './common'; import { getTimesliceTargetComparator, getFilterRange } from './common'; export class ApmTransactionDurationTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + public async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { if (!apmTransactionDurationIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -34,7 +39,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - this.buildSource(slo, slo.indicator), + await this.buildSource(slo, slo.indicator, dataViewService), this.buildDestination(), this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo, slo.indicator), @@ -70,7 +75,11 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); } - private buildSource(slo: SLODefinition, indicator: APMTransactionDurationIndicator) { + private async buildSource( + slo: SLODefinition, + indicator: APMTransactionDurationIndicator, + dataViewService: DataViewsService + ) { const queryFilter: estypes.QueryDslQueryContainer[] = [getFilterRange(slo, '@timestamp')]; if (indicator.params.service !== ALL_VALUE) { @@ -104,14 +113,18 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator }, }); } + const dataView = await this.getIndicatorDataView({ + dataViewService, + dataViewId: indicator.params.dataViewId, + }); if (!!indicator.params.filter) { - queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter)); + queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter, dataView)); } return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo), + runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts index 5706521cdf2f68..13c73443960afe 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts @@ -13,8 +13,10 @@ import { createSLOWithTimeslicesBudgetingMethod, } from '../fixtures/slo'; import { ApmTransactionErrorRateTransformGenerator } from './apm_transaction_error_rate'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; const generator = new ApmTransactionErrorRateTransformGenerator(); +const spaceId = 'custom-space'; describe('APM Transaction Error Rate Transform Generator', () => { it('returns the expected transform params with every specified indicator params', async () => { @@ -22,7 +24,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { id: 'irrelevant', indicator: createAPMTransactionErrorRateIndicator(), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -32,7 +34,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { id: 'irrelevant', indicator: createAPMTransactionErrorRateIndicator(), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -47,7 +49,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -61,7 +63,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { transactionType: ALL_VALUE, }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); @@ -73,7 +75,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { index, }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.index).toEqual(index); }); @@ -85,12 +87,12 @@ describe('APM Transaction Error Rate Transform Generator', () => { filter, }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); - it("groups by the 'service.name'", () => { + it("groups by the 'service.name'", async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ service: 'my-service', @@ -100,13 +102,13 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("groups by the 'service.environment'", () => { + it("groups by the 'service.environment'", async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ service: ALL_VALUE, @@ -116,13 +118,13 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("groups by the 'transaction.name'", () => { + it("groups by the 'transaction.name'", async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ service: ALL_VALUE, @@ -132,13 +134,13 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("groups by the 'transaction.type'", () => { + it("groups by the 'transaction.type'", async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ service: ALL_VALUE, @@ -148,13 +150,13 @@ describe('APM Transaction Error Rate Transform Generator', () => { }), }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); expect(transform.pivot?.group_by).toMatchSnapshot(); }); - it("overrides the range filter when 'preventInitialBackfill' is true", () => { + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator(), settings: { @@ -164,7 +166,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts index 59b753711a0d89..863a31cfc508c8 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts @@ -12,6 +12,7 @@ import { timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; import { estypes } from '@elastic/elasticsearch'; +import { DataViewsService } from '@kbn/data-views-plugin/common'; import { getElasticsearchQueryOrThrow, TransformGenerator } from '.'; import { getSLOTransformId, @@ -24,7 +25,11 @@ import { InvalidTransformError } from '../../errors'; import { parseIndex, getTimesliceTargetComparator, getFilterRange } from './common'; export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + public async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { if (!apmTransactionErrorRateIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -32,7 +37,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - this.buildSource(slo, slo.indicator), + await this.buildSource(slo, slo.indicator, dataViewService), this.buildDestination(), this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo), @@ -68,7 +73,11 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); } - private buildSource(slo: SLODefinition, indicator: APMTransactionErrorRateIndicator) { + private async buildSource( + slo: SLODefinition, + indicator: APMTransactionErrorRateIndicator, + dataViewService: DataViewsService + ) { const queryFilter: estypes.QueryDslQueryContainer[] = [getFilterRange(slo, '@timestamp')]; if (indicator.params.service !== ALL_VALUE) { @@ -103,13 +112,18 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato }); } + const dataView = await this.getIndicatorDataView({ + dataViewService, + dataViewId: indicator.params.dataViewId, + }); + if (indicator.params.filter) { - queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter)); + queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter, dataView)); } return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo), + runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts index 283bc375a0db0c..35a6e180650fab 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts @@ -8,17 +8,18 @@ import { buildEsQuery, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { QuerySchema, kqlQuerySchema } from '@kbn/slo-schema'; import { Logger } from '@kbn/logging'; +import { DataView } from '@kbn/data-views-plugin/common'; import { SLODefinition } from '../../domain/models'; import { getDelayInSecondsFromSLO } from '../../domain/services/get_delay_in_seconds_from_slo'; import { InvalidTransformError } from '../../errors'; -export function getElasticsearchQueryOrThrow(kuery: QuerySchema = '') { +export function getElasticsearchQueryOrThrow(kuery: QuerySchema = '', dataView?: DataView) { try { if (kqlQuerySchema.is(kuery)) { return toElasticsearchQuery(fromKueryExpression(kuery)); } else { return buildEsQuery( - undefined, + dataView, { query: kuery?.kqlQuery, language: 'kuery', diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts index 2307956564ba3c..2de75b8f7d86cf 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts @@ -12,12 +12,14 @@ import { createSLOWithTimeslicesBudgetingMethod, } from '../fixtures/slo'; import { HistogramTransformGenerator } from './histogram'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; const generator = new HistogramTransformGenerator(); +const spaceId = 'custom-space'; describe('Histogram Transform Generator', () => { describe('validation', () => { - it('throws when the good filter is invalid', () => { + it('throws when the good filter is invalid', async () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ good: { @@ -29,10 +31,13 @@ describe('Histogram Transform Generator', () => { }, }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); + + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL: foo:/ + ); }); - it('throws when the total filter is invalid', () => { + it('throws when the total filter is invalid', async () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ good: { @@ -42,20 +47,25 @@ describe('Histogram Transform Generator', () => { }, }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); + + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL: foo:/ + ); }); - it('throws when the query_filter is invalid', () => { + it('throws when the query_filter is invalid', async () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ filter: '{ kql.query: invalid' }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL/ + ); }); }); it('returns the expected transform params with every specified indicator params', async () => { const anSLO = createSLO({ id: 'irrelevant', indicator: createHistogramIndicator() }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -65,7 +75,7 @@ describe('Histogram Transform Generator', () => { id: 'irrelevant', indicator: createHistogramIndicator(), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -80,7 +90,7 @@ describe('Histogram Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -89,7 +99,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ filter: 'labels.groupId: group-4' }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); @@ -98,7 +108,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ index: 'my-own-index*' }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.index).toBe('my-own-index*'); }); @@ -109,7 +119,7 @@ describe('Histogram Transform Generator', () => { timestampField: 'my-date-field', }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -120,7 +130,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator(), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -137,7 +147,7 @@ describe('Histogram Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -146,7 +156,7 @@ describe('Histogram Transform Generator', () => { const anSLO = createSLO({ indicator: createHistogramIndicator(), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -161,12 +171,12 @@ describe('Histogram Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); - it("overrides the range filter when 'preventInitialBackfill' is true", () => { + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { const slo = createSLO({ indicator: createHistogramIndicator(), settings: { @@ -176,7 +186,7 @@ describe('Histogram Transform Generator', () => { }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts index c8cfb52239f896..76325445a862cb 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts @@ -11,20 +11,26 @@ import { histogramIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; + +import { DataViewsService } from '@kbn/data-views-plugin/common'; +import { InvalidTransformError } from '../../errors'; +import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; import { getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, } from '../../../common/constants'; -import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { SLODefinition } from '../../domain/models'; -import { InvalidTransformError } from '../../errors'; import { GetHistogramIndicatorAggregation } from '../aggregations'; import { getTimesliceTargetComparator, getFilterRange } from './common'; export class HistogramTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + public async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { if (!histogramIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -32,7 +38,7 @@ export class HistogramTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - this.buildSource(slo, slo.indicator), + await this.buildSource(slo, slo.indicator, dataViewService), this.buildDestination(), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -45,15 +51,24 @@ export class HistogramTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLODefinition, indicator: HistogramIndicator) { + private async buildSource( + slo: SLODefinition, + indicator: HistogramIndicator, + dataViewService: DataViewsService + ) { + const dataView = await this.getIndicatorDataView({ + dataViewService, + dataViewId: indicator.params.index, + }); + return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo), + runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), query: { bool: { filter: [ getFilterRange(slo, indicator.params.timestampField), - getElasticsearchQueryOrThrow(indicator.params.filter), + getElasticsearchQueryOrThrow(indicator.params.filter, dataView), ], }, }, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts index 4b74695c8942e3..c41e0d4b3df530 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts @@ -12,34 +12,42 @@ import { createSLOWithTimeslicesBudgetingMethod, } from '../fixtures/slo'; import { KQLCustomTransformGenerator } from './kql_custom'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; const generator = new KQLCustomTransformGenerator(); +const spaceId = 'custom-space'; describe('KQL Custom Transform Generator', () => { describe('validation', () => { - it('throws when the KQL numerator is invalid', () => { + it('throws when the KQL numerator is invalid', async () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ good: '{ kql.query: invalid' }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL/ + ); }); - it('throws when the KQL denominator is invalid', () => { + it('throws when the KQL denominator is invalid', async () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ total: '{ kql.query: invalid' }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL/ + ); }); - it('throws when the KQL query_filter is invalid', () => { + it('throws when the KQL query_filter is invalid', async () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ filter: '{ kql.query: invalid' }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL/ + ); }); }); it('returns the expected transform params with every specified indicator params', async () => { const anSLO = createSLO({ id: 'irrelevant', indicator: createKQLCustomIndicator() }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -49,7 +57,7 @@ describe('KQL Custom Transform Generator', () => { id: 'irrelevant', indicator: createKQLCustomIndicator(), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -64,7 +72,7 @@ describe('KQL Custom Transform Generator', () => { timesliceWindow: twoMinute(), }, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -73,7 +81,7 @@ describe('KQL Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ filter: 'labels.groupId: group-4' }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); @@ -82,7 +90,7 @@ describe('KQL Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ index: 'my-own-index*' }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.index).toBe('my-own-index*'); }); @@ -93,7 +101,7 @@ describe('KQL Custom Transform Generator', () => { timestampField: 'my-date-field', }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -106,7 +114,7 @@ describe('KQL Custom Transform Generator', () => { good: 'latency < 400 and (http.status_code: 2xx or http.status_code: 3xx or http.status_code: 4xx)', }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -117,12 +125,12 @@ describe('KQL Custom Transform Generator', () => { total: 'http.status_code: *', }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); - it("overrides the range filter when 'preventInitialBackfill' is true", () => { + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { const slo = createSLO({ indicator: createKQLCustomIndicator(), settings: { @@ -132,7 +140,7 @@ describe('KQL Custom Transform Generator', () => { }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts index 573a83a5400de9..d721140f243cf7 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts @@ -7,19 +7,25 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { kqlCustomIndicatorSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; + +import { DataViewsService } from '@kbn/data-views-plugin/common'; +import { InvalidTransformError } from '../../errors'; +import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; import { getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, } from '../../../common/constants'; -import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { KQLCustomIndicator, SLODefinition } from '../../domain/models'; -import { InvalidTransformError } from '../../errors'; import { getTimesliceTargetComparator, getFilterRange } from './common'; export class KQLCustomTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + public async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { if (!kqlCustomIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -27,7 +33,7 @@ export class KQLCustomTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - this.buildSource(slo, slo.indicator), + await this.buildSource(slo, slo.indicator, dataViewService), this.buildDestination(), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -40,10 +46,18 @@ export class KQLCustomTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLODefinition, indicator: KQLCustomIndicator) { + private async buildSource( + slo: SLODefinition, + indicator: KQLCustomIndicator, + dataViewService: DataViewsService + ) { + const dataView = await this.getIndicatorDataView({ + dataViewService, + dataViewId: indicator.params.dataViewId, + }); return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo), + runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts index 1f6a1638baf1e3..85da6de832c982 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.test.ts @@ -12,12 +12,14 @@ import { createSLOWithTimeslicesBudgetingMethod, } from '../fixtures/slo'; import { MetricCustomTransformGenerator } from './metric_custom'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; const generator = new MetricCustomTransformGenerator(); +const spaceId = 'custom-space'; describe('Metric Custom Transform Generator', () => { describe('validation', () => { - it('throws when the good equation is invalid', () => { + it('throws when the good equation is invalid', async () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ good: { @@ -26,9 +28,11 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid equation/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid equation/ + ); }); - it('throws when the good filter is invalid', () => { + it('throws when the good filter is invalid', async () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ good: { @@ -37,9 +41,11 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL: foo:/ + ); }); - it('throws when the total equation is invalid', () => { + it('throws when the total equation is invalid', async () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ total: { @@ -48,9 +54,11 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid equation/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid equation/ + ); }); - it('throws when the total filter is invalid', () => { + it('throws when the total filter is invalid', async () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ total: { @@ -59,19 +67,23 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); + await expect(() => + generator.getTransformParams(anSLO, spaceId, dataViewsService) + ).rejects.toThrow(/Invalid KQL: foo:/); }); - it('throws when the query_filter is invalid', () => { + it('throws when the query_filter is invalid', async () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ filter: '{ kql.query: invalid' }), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL/); + await expect(() => + generator.getTransformParams(anSLO, spaceId, dataViewsService) + ).rejects.toThrow(/Invalid KQL/); }); }); it('returns the expected transform params with every specified indicator params', async () => { const anSLO = createSLO({ id: 'irrelevant', indicator: createMetricCustomIndicator() }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -81,7 +93,7 @@ describe('Metric Custom Transform Generator', () => { id: 'irrelevant', indicator: createMetricCustomIndicator(), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -90,7 +102,7 @@ describe('Metric Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ filter: 'labels.groupId: group-4' }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); @@ -99,7 +111,7 @@ describe('Metric Custom Transform Generator', () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ index: 'my-own-index*' }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.index).toBe('my-own-index*'); }); @@ -110,7 +122,7 @@ describe('Metric Custom Transform Generator', () => { timestampField: 'my-date-field', }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -126,7 +138,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -140,7 +152,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -156,7 +168,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -170,7 +182,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); @@ -184,7 +196,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -198,7 +210,7 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); @@ -212,12 +224,12 @@ describe('Metric Custom Transform Generator', () => { }, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); - it("overrides the range filter when 'preventInitialBackfill' is true", () => { + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { const slo = createSLO({ indicator: createMetricCustomIndicator(), settings: { @@ -227,7 +239,7 @@ describe('Metric Custom Transform Generator', () => { }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts index d004b3e01d890a..c6127b600a4ebe 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts @@ -7,22 +7,28 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { metricCustomIndicatorSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; + +import { DataViewsService } from '@kbn/data-views-plugin/common'; +import { InvalidTransformError } from '../../errors'; +import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; import { getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, } from '../../../common/constants'; -import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { MetricCustomIndicator, SLODefinition } from '../../domain/models'; -import { InvalidTransformError } from '../../errors'; import { GetCustomMetricIndicatorAggregation } from '../aggregations'; import { getTimesliceTargetComparator, getFilterRange } from './common'; export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; export class MetricCustomTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + public async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { if (!metricCustomIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -30,7 +36,7 @@ export class MetricCustomTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - this.buildSource(slo, slo.indicator), + await this.buildSource(slo, slo.indicator, dataViewService), this.buildDestination(), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -43,15 +49,23 @@ export class MetricCustomTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLODefinition, indicator: MetricCustomIndicator) { + private async buildSource( + slo: SLODefinition, + indicator: MetricCustomIndicator, + dataViewService: DataViewsService + ) { + const dataView = await this.getIndicatorDataView({ + dataViewService, + dataViewId: indicator.params.dataViewId, + }); return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo), + runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), query: { bool: { filter: [ getFilterRange(slo, indicator.params.timestampField), - getElasticsearchQueryOrThrow(indicator.params.filter), + getElasticsearchQueryOrThrow(indicator.params.filter, dataView), ], }, }, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts index 2a3a6f188eaa9a..b292b8d12f342f 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts @@ -6,6 +6,7 @@ */ import { ALL_VALUE } from '@kbn/slo-schema'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; import { SLODefinition } from '../../domain/models'; import { createSLO, createSyntheticsAvailabilityIndicator } from '../fixtures/slo'; import { SyntheticsAvailabilityTransformGenerator } from './synthetics_availability'; @@ -17,9 +18,9 @@ const generator = new SyntheticsAvailabilityTransformGenerator(); describe('Synthetics Availability Transform Generator', () => { const spaceId = 'custom-space'; - it('returns the expected transform params', () => { + it('returns the expected transform params', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator() }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform).toEqual({ _meta: { @@ -164,12 +165,12 @@ describe('Synthetics Availability Transform Generator', () => { }); }); - it('groups by config id and observer.name when using default groupings', () => { + it('groups by config id and observer.name when using default groupings', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator(), }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.pivot?.group_by).toEqual( expect.objectContaining({ @@ -187,13 +188,13 @@ describe('Synthetics Availability Transform Generator', () => { ); }); - it('does not include config id and observer.name when using non default groupings', () => { + it('does not include config id and observer.name when using non default groupings', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator(), groupBy: ['host.name'], }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.pivot?.group_by).not.toEqual( expect.objectContaining({ @@ -223,13 +224,13 @@ describe('Synthetics Availability Transform Generator', () => { it.each([[[]], [[ALL_VALUE]]])( 'adds observer.geo.name and monitor.name to groupings key by default, multi group by', - (groupBy) => { + async (groupBy) => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator(), groupBy, }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.pivot?.group_by).toEqual( expect.objectContaining({ @@ -250,13 +251,13 @@ describe('Synthetics Availability Transform Generator', () => { it.each([[''], [ALL_VALUE]])( 'adds observer.geo.name and monitor.name to groupings key by default, single group by', - (groupBy) => { + async (groupBy) => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator(), groupBy, }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.pivot?.group_by).toEqual( expect.objectContaining({ @@ -275,13 +276,13 @@ describe('Synthetics Availability Transform Generator', () => { } ); - it.each([['host.name'], [['host.name']]])('handles custom groupBy', (groupBy) => { + it.each([['host.name'], [['host.name']]])('handles custom groupBy', async (groupBy) => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator(), groupBy, }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.pivot?.group_by).toEqual( expect.objectContaining({ @@ -294,9 +295,9 @@ describe('Synthetics Availability Transform Generator', () => { ); }); - it('filters by summary.final_attempt', () => { + it('filters by summary.final_attempt', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator() }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query?.bool?.filter).toContainEqual({ term: { @@ -305,7 +306,7 @@ describe('Synthetics Availability Transform Generator', () => { }); }); - it('adds tag filters', () => { + it('adds tag filters', async () => { const tags = [ { value: 'tag-1', label: 'tag1' }, { value: 'tag-2', label: 'tag2' }, @@ -321,7 +322,7 @@ describe('Synthetics Availability Transform Generator', () => { }, } as SLODefinition['indicator'], }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query?.bool?.filter).toContainEqual({ terms: { @@ -335,7 +336,7 @@ describe('Synthetics Availability Transform Generator', () => { }); }); - it('adds monitorId filter', () => { + it('adds monitorId filter', async () => { const monitorIds = [ { value: 'id-1', label: 'Monitor name 1' }, { value: 'id-2', label: 'Monitor name 2' }, @@ -351,7 +352,7 @@ describe('Synthetics Availability Transform Generator', () => { }, } as SLODefinition['indicator'], }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query?.bool?.filter).toContainEqual({ terms: { @@ -365,7 +366,7 @@ describe('Synthetics Availability Transform Generator', () => { }); }); - it('adds project id filter', () => { + it('adds project id filter', async () => { const projects = [ { value: 'id-1', label: 'Project name 1' }, { value: 'id-2', label: 'Project name 2' }, @@ -381,7 +382,7 @@ describe('Synthetics Availability Transform Generator', () => { }, } as SLODefinition['indicator'], }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query?.bool?.filter).toContainEqual({ terms: { @@ -395,9 +396,9 @@ describe('Synthetics Availability Transform Generator', () => { }); }); - it('filters by space', () => { + it('filters by space', async () => { const slo = createSLO({ id: 'irrelevant', indicator: createSyntheticsAvailabilityIndicator() }); - const transform = generator.getTransformParams(slo, spaceId); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); expect(transform.source.query?.bool?.filter).toContainEqual({ term: { @@ -406,7 +407,7 @@ describe('Synthetics Availability Transform Generator', () => { }); }); - it("overrides the range filter when 'preventInitialBackfill' is true", () => { + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { const slo = createSLO({ indicator: createSyntheticsAvailabilityIndicator(), settings: { @@ -416,7 +417,7 @@ describe('Synthetics Availability Transform Generator', () => { }, }); - const transform = generator.getTransformParams(slo, 'default'); + const transform = await generator.getTransformParams(slo, 'default', dataViewsService); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts index a98f2a2b3e2320..5839a5611351eb 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts @@ -13,6 +13,7 @@ import { occurrencesBudgetingMethodSchema, SyntheticsAvailabilityIndicator, } from '@kbn/slo-schema'; +import { DataViewsService } from '@kbn/data-views-plugin/common'; import { getElasticsearchQueryOrThrow, TransformGenerator } from '.'; import { getSLOTransformId, @@ -27,7 +28,11 @@ import { SLODefinition } from '../../domain/models'; import { getFilterRange } from './common'; export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLODefinition, spaceId: string): TransformPutTransformRequest { + public async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { if (!syntheticsAvailabilityIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -35,7 +40,7 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - this.buildSource(slo, slo.indicator, spaceId), + await this.buildSource(slo, slo.indicator, spaceId, dataViewService), this.buildDestination(), this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo), @@ -102,10 +107,11 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator ); } - private buildSource( + private async buildSource( slo: SLODefinition, indicator: SyntheticsAvailabilityIndicator, - spaceId: string + spaceId: string, + dataViewService: DataViewsService ) { const queryFilter: estypes.QueryDslQueryContainer[] = [ { term: { 'summary.final_attempt': true } }, @@ -146,11 +152,14 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator queryFilter.push(getElasticsearchQueryOrThrow(indicator.params.filter)); } + const dataView = await this.getIndicatorDataView({ + dataViewService, + dataViewId: indicator.params.dataViewId, + }); + return { index: SYNTHETICS_INDEX_PATTERN, - runtime_mappings: { - ...this.buildCommonRuntimeMappings(slo), - }, + runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), query: { bool: { filter: queryFilter, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts index 1fcd8c6abe892b..02c69f38d67058 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.test.ts @@ -12,8 +12,11 @@ import { createSLO, } from '../fixtures/slo'; import { TimesliceMetricTransformGenerator } from './timeslice_metric'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; const generator = new TimesliceMetricTransformGenerator(); +const spaceId = 'custom-space'; + const everythingIndicator = createTimesliceMetricIndicator( [ { name: 'A', aggregation: 'avg', field: 'test.field', filter: 'test.category: "test"' }, @@ -28,36 +31,40 @@ const everythingIndicator = createTimesliceMetricIndicator( describe('Timeslice Metric Transform Generator', () => { describe('validation', () => { - it('throws when the budgeting method is occurrences', () => { + it('throws when the budgeting method is occurrences', async () => { const anSLO = createSLO({ indicator: createTimesliceMetricIndicator( [{ name: 'A', aggregation: 'avg', field: 'test.field' }], '(A / 200) + A' ), }); - expect(() => generator.getTransformParams(anSLO)).toThrow( + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( 'The sli.metric.timeslice indicator MUST have a timeslice budgeting method.' ); }); - it('throws when the metric equation is invalid', () => { + it('throws when the metric equation is invalid', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ indicator: createTimesliceMetricIndicator( [{ name: 'A', aggregation: 'avg', field: 'test.field' }], '(a / 200) + A' ), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid equation/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid equation/ + ); }); - it('throws when the metric filter is invalid', () => { + it('throws when the metric filter is invalid', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ indicator: createTimesliceMetricIndicator( [{ name: 'A', aggregation: 'avg', field: 'test.field', filter: 'test:' }], '(A / 200) + A' ), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: test:/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL: test:/ + ); }); - it('throws when the query_filter is invalid', () => { + it('throws when the query_filter is invalid', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ indicator: createTimesliceMetricIndicator( [{ name: 'A', aggregation: 'avg', field: 'test.field', filter: 'test.category: "test"' }], @@ -65,7 +72,9 @@ describe('Timeslice Metric Transform Generator', () => { 'test:' ), }); - expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL/); + await expect(generator.getTransformParams(anSLO, spaceId, dataViewsService)).rejects.toThrow( + /Invalid KQL/ + ); }); }); @@ -74,7 +83,7 @@ describe('Timeslice Metric Transform Generator', () => { id: 'irrelevant', indicator: everythingIndicator, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -84,7 +93,7 @@ describe('Timeslice Metric Transform Generator', () => { id: 'irrelevant', indicator: everythingIndicator, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform).toMatchSnapshot(); }); @@ -93,7 +102,7 @@ describe('Timeslice Metric Transform Generator', () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ indicator: everythingIndicator, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.query).toMatchSnapshot(); }); @@ -105,7 +114,7 @@ describe('Timeslice Metric Transform Generator', () => { params: { ...everythingIndicator.params, index: 'my-own-index*' }, }, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.source.index).toBe('my-own-index*'); }); @@ -117,7 +126,7 @@ describe('Timeslice Metric Transform Generator', () => { params: { ...everythingIndicator.params, timestampField: 'my-date-field' }, }, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.sync?.time?.field).toBe('my-date-field'); // @ts-ignore @@ -128,7 +137,7 @@ describe('Timeslice Metric Transform Generator', () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ indicator: everythingIndicator, }); - const transform = generator.getTransformParams(anSLO); + const transform = await generator.getTransformParams(anSLO, spaceId, dataViewsService); expect(transform.pivot!.aggregations!._metric).toEqual({ bucket_script: { @@ -166,7 +175,7 @@ describe('Timeslice Metric Transform Generator', () => { }); }); - it("overrides the range filter when 'preventInitialBackfill' is true", () => { + it("overrides the range filter when 'preventInitialBackfill' is true", async () => { const slo = createSLOWithTimeslicesBudgetingMethod({ indicator: everythingIndicator, settings: { @@ -176,7 +185,7 @@ describe('Timeslice Metric Transform Generator', () => { }, }); - const transform = generator.getTransformParams(slo); + const transform = await generator.getTransformParams(slo, spaceId, dataViewsService); // @ts-ignore const rangeFilter = transform.source.query.bool.filter.find((f) => 'range' in f); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts index 719766b0c4efe2..579772b0eb1de4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts @@ -12,6 +12,7 @@ import { timesliceMetricIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; +import { DataViewsService } from '@kbn/data-views-plugin/common'; import { InvalidTransformError } from '../../errors'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; @@ -27,7 +28,11 @@ import { getFilterRange } from './common'; const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; export class TimesliceMetricTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + public async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { if (!timesliceMetricIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -35,7 +40,7 @@ export class TimesliceMetricTransformGenerator extends TransformGenerator { return getSLOTransformTemplate( this.buildTransformId(slo), this.buildDescription(slo), - this.buildSource(slo, slo.indicator), + await this.buildSource(slo, slo.indicator, dataViewService), this.buildDestination(), this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), @@ -48,15 +53,23 @@ export class TimesliceMetricTransformGenerator extends TransformGenerator { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLODefinition, indicator: TimesliceMetricIndicator) { + private async buildSource( + slo: SLODefinition, + indicator: TimesliceMetricIndicator, + dataViewService: DataViewsService + ) { + const dataView = await this.getIndicatorDataView({ + dataViewService, + dataViewId: indicator.params.index, + }); return { index: parseIndex(indicator.params.index), - runtime_mappings: this.buildCommonRuntimeMappings(slo), + runtime_mappings: this.buildCommonRuntimeMappings(slo, dataView), query: { bool: { filter: [ getFilterRange(slo, indicator.params.timestampField), - getElasticsearchQueryOrThrow(indicator.params.filter), + getElasticsearchQueryOrThrow(indicator.params.filter, dataView), ], }, }, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts index 0a2226d98d55ad..37e20144435d7a 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts @@ -10,16 +10,18 @@ import { TransformPutTransformRequest, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALL_VALUE, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; +import { DataView, DataViewsService } from '@kbn/data-views-plugin/common'; import { TransformSettings } from '../../assets/transform_templates/slo_transform_template'; import { SLODefinition } from '../../domain/models'; export abstract class TransformGenerator { public abstract getTransformParams( slo: SLODefinition, - spaceId: string - ): TransformPutTransformRequest; + spaceId: string, + dataViewService: DataViewsService + ): Promise; - public buildCommonRuntimeMappings(slo: SLODefinition): MappingRuntimeFields { + public buildCommonRuntimeMappings(slo: SLODefinition, dataView?: DataView): MappingRuntimeFields { return { 'slo.id': { type: 'keyword', @@ -33,6 +35,7 @@ export abstract class TransformGenerator { source: `emit(${slo.revision})`, }, }, + ...(dataView?.getRuntimeMappings?.() ?? {}), }; } @@ -81,6 +84,24 @@ export abstract class TransformGenerator { }; } + public async getIndicatorDataView({ + dataViewService, + dataViewId, + }: { + dataViewService: DataViewsService; + dataViewId?: string; + }): Promise { + let dataView: DataView | undefined; + if (dataViewId) { + try { + dataView = await dataViewService.get(dataViewId); + } catch (e) { + // If the data view is not found, we will continue without it + } + } + return dataView; + } + public buildSettings( slo: SLODefinition, sourceIndexTimestampField: string | undefined = '@timestamp' diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts index d7758f0fbdd4a3..7e6cf50c169fa0 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts @@ -26,6 +26,8 @@ import { createAPMTransactionErrorRateIndicator, createSLO, } from './fixtures/slo'; +import { dataViewsService } from '@kbn/data-views-plugin/server/mocks'; +import { DataViewsService } from '@kbn/data-views-plugin/common'; describe('TransformManager', () => { let esClientMock: ElasticsearchClientMock; @@ -44,7 +46,13 @@ describe('TransformManager', () => { const generators: Record = { 'sli.apm.transactionDuration': new DummyTransformGenerator(), }; - const service = new DefaultTransformManager(generators, esClientMock, loggerMock, spaceId); + const service = new DefaultTransformManager( + generators, + esClientMock, + loggerMock, + spaceId, + dataViewsService + ); await expect( service.install(createSLO({ indicator: createAPMTransactionErrorRateIndicator() })) @@ -60,7 +68,8 @@ describe('TransformManager', () => { generators, esClientMock, loggerMock, - spaceId + spaceId, + dataViewsService ); await expect( @@ -80,7 +89,8 @@ describe('TransformManager', () => { generators, esClientMock, loggerMock, - spaceId + spaceId, + dataViewsService ); const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() }); @@ -101,7 +111,8 @@ describe('TransformManager', () => { generators, esClientMock, loggerMock, - spaceId + spaceId, + dataViewsService ); await transformManager.preview('slo-transform-id'); @@ -120,7 +131,8 @@ describe('TransformManager', () => { generators, esClientMock, loggerMock, - spaceId + spaceId, + dataViewsService ); await transformManager.start('slo-transform-id'); @@ -139,7 +151,8 @@ describe('TransformManager', () => { generators, esClientMock, loggerMock, - spaceId + spaceId, + dataViewsService ); await transformManager.stop('slo-transform-id'); @@ -158,7 +171,8 @@ describe('TransformManager', () => { generators, esClientMock, loggerMock, - spaceId + spaceId, + dataViewsService ); await transformManager.uninstall('slo-transform-id'); @@ -178,7 +192,8 @@ describe('TransformManager', () => { generators, esClientMock, loggerMock, - spaceId + spaceId, + dataViewsService ); await transformManager.uninstall('slo-transform-id'); @@ -189,13 +204,21 @@ describe('TransformManager', () => { }); class DummyTransformGenerator extends TransformGenerator { - getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + async getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { return {} as TransformPutTransformRequest; } } class FailTransformGenerator extends TransformGenerator { - getTransformParams(slo: SLODefinition): TransformPutTransformRequest { + getTransformParams( + slo: SLODefinition, + spaceId: string, + dataViewService: DataViewsService + ): Promise { throw new Error('Some error'); } } diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts index e00d67e9944bbb..f90d240e300871 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts @@ -8,6 +8,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { DataViewsService } from '@kbn/data-views-plugin/server'; import { SLODefinition, IndicatorTypes } from '../domain/models'; import { SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; @@ -17,7 +18,7 @@ type TransformId = string; export interface TransformManager { install(slo: SLODefinition): Promise; - inspect(slo: SLODefinition): TransformPutTransformRequest; + inspect(slo: SLODefinition): Promise; preview(transformId: TransformId): Promise; start(transformId: TransformId): Promise; stop(transformId: TransformId): Promise; @@ -29,7 +30,8 @@ export class DefaultTransformManager implements TransformManager { private generators: Record, private esClient: ElasticsearchClient, private logger: Logger, - private spaceId: string + private spaceId: string, + private dataViewService: DataViewsService ) {} async install(slo: SLODefinition): Promise { @@ -39,7 +41,11 @@ export class DefaultTransformManager implements TransformManager { throw new Error(`Unsupported indicator type [${slo.indicator.type}]`); } - const transformParams = generator.getTransformParams(slo, this.spaceId); + const transformParams = await generator.getTransformParams( + slo, + this.spaceId, + this.dataViewService + ); try { await retryTransientEsErrors(() => this.esClient.transform.putTransform(transformParams), { logger: this.logger, @@ -56,14 +62,14 @@ export class DefaultTransformManager implements TransformManager { return transformParams.transform_id; } - inspect(slo: SLODefinition): TransformPutTransformRequest { + async inspect(slo: SLODefinition): Promise { const generator = this.generators[slo.indicator.type]; if (!generator) { this.logger.error(`No transform generator found for indicator type [${slo.indicator.type}]`); throw new Error(`Unsupported indicator type [${slo.indicator.type}]`); } - return generator.getTransformParams(slo, this.spaceId); + return await generator.getTransformParams(slo, this.spaceId, this.dataViewService); } async preview(transformId: string): Promise {