[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>
This commit is contained in:
Shahzad 2024-06-07 14:17:21 +02:00 committed by GitHub
parent 217e29dc2a
commit f3fdb0f398
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1017 additions and 482 deletions

View file

@ -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,
}),
]),
});

View file

@ -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<ObservabilityPluginSetup> {
...plugins,
core,
},
dataViews: pluginStart.dataViews,
spaces: pluginStart.spaces,
ruleDataService,
assistant: {

View file

@ -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: {

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -22,6 +22,7 @@
"dataViews",
"lens",
"dataViewEditor",
"dataViewFieldEditor",
"fieldFormats",
"observability",
"observabilityShared",

View file

@ -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<DataView | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(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) };
}

View file

@ -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(<div>Tags list</div>);
@ -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,

View file

@ -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<CreateSLOForm<APMTransactionErrorRateIndicator>>();
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() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="apmLatencyFilterInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.apmLatency.filter', {
defaultMessage: 'Query filter',
})}

View file

@ -10,6 +10,7 @@ import { APMTransactionDurationIndicator } from '@kbn/slo-schema';
import { i18n } from '@kbn/i18n';
import React, { useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { DATA_VIEW_FIELD } from '../custom_common/index_selection';
import { GroupByField } from '../common/group_by_field';
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
import { useFetchApmIndex } from '../../../../hooks/use_fetch_apm_indices';
@ -52,8 +53,11 @@ export function ApmLatencyIndicatorTypeForm() {
}
}, [setValue, apmIndex]);
const dataViewId = watch(DATA_VIEW_FIELD);
const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({
indexPatternString: apmIndex,
dataViewId,
});
return (
@ -164,7 +168,7 @@ export function ApmLatencyIndicatorTypeForm() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="apmLatencyFilterInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.apmLatency.filter', {
defaultMessage: 'Query filter',
})}

View file

@ -9,14 +9,15 @@ import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { ReactNode, useState } from 'react';
import { FieldPath } from 'react-hook-form';
import { DataView } from '@kbn/data-views-plugin/common';
import { RunTimeFieldUsed } from './runtime_field_used';
import { QuerySearchBar } from './query_search_bar';
import { QueryDocumentsFlyout } from './query_documents_flyout';
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
import { CreateSLOForm } from '../../types';
export interface SearchBarProps {
dataTestSubj: string;
indexPatternString: string | undefined;
dataView?: DataView;
label: string;
name: FieldPath<CreateSLOForm>;
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}
/>
)}
<RunTimeFieldUsed dataView={dataView} name={name} />
</>
);
}

View file

@ -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<CreateSLOForm>();

View file

@ -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<CreateSLOForm>;
}) {
const fieldNames = useRunTimeFieldBeingUsed(name, dataView);
if (fieldNames.length === 0) {
return null;
}
return (
<>
<EuiSpacer size="s" />
<EuiCallOut
title={i18n.translate('xpack.slo.runTimeFieldUsed.euiCallOut.runtimeFieldsBeingUsedLabel', {
defaultMessage: 'Runtime fields being used',
})}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.slo.runTimeFieldUsed.p.theRuntimeFieldLabel"
defaultMessage="The runtime field(s) {fields} from kibana dataview are being used in the query. If you update the runtime field, the query will not be updated automatically. You must save the slo definition again to update the underlying transform query."
values={{
fields: <strong>{fieldNames.join(', ')}</strong>,
}}
/>
</p>
</EuiCallOut>
</>
);
}

View file

@ -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() {
export const DATA_VIEW_FIELD = 'indicator.params.dataViewId';
const INDEX_FIELD = 'indicator.params.index';
const TIMESTAMP_FIELD = 'indicator.params.timestampField';
export function IndexSelection({ selectedDataView }: { selectedDataView?: DataView }) {
const { control, getFieldState, setValue, watch } = useFormContext<CreateSLOForm>();
const { dataViews: dataViewsService } = useKibana().services;
const { dataViews: dataViewsService, dataViewFieldEditor } = useKibana().services;
const { isLoading: isDataViewsLoading, data: dataViews = [], refetch } = useFetchDataViews();
const { dataViewEditor } = useKibana<SloPublicPluginsStart>().services;
const { dataViewEditor } = useKibana().services;
const currentIndexPattern = watch(INDEX_FIELD);
const currentDataViewId = watch(DATA_VIEW_FIELD);
const [adHocDataViews, setAdHocDataViews] = useState<DataView[]>([]);
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 (
<EuiFormRow label={INDEX_LABEL} isInvalid={getFieldState('indicator.params.index').invalid}>
<EuiFormRow label={INDEX_LABEL} isInvalid={getFieldState(INDEX_FIELD).invalid}>
<Controller
defaultValue=""
name="indicator.params.index"
name={DATA_VIEW_FIELD}
control={control}
rules={{ required: true }}
rules={{ required: !Boolean(currentIndexPattern) }}
render={({ field, fieldState }) => (
<DataViewPicker
adHocDataViews={adHocDataViews}
trigger={{
label: field.value || SELECT_DATA_VIEW,
label: currentIndexPattern || SELECT_DATA_VIEW,
fullWidth: true,
color: fieldState.invalid ? 'danger' : 'text',
isLoading: isDataViewsLoading,
'data-test-subj': 'indexSelection',
}}
onChangeDataView={(newId: string) => {
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);
}
},
});

View file

@ -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<DataView[]>([]);
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,
};
};

View file

@ -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<CreateSLOForm>();
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() {
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexGroup direction="row" gutterSize="l">
<EuiFlexItem>
<IndexSelection />
<IndexSelection selectedDataView={dataView} />
</EuiFlexItem>
<IndexFieldSelector
@ -51,7 +53,7 @@ export function CustomKqlIndicatorTypeForm() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="customKqlIndicatorFormQueryFilterInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.sliType.customKql.queryFilter', {
defaultMessage: 'Query filter',
})}
@ -74,7 +76,7 @@ export function CustomKqlIndicatorTypeForm() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="customKqlIndicatorFormGoodQueryInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.sliType.customKql.goodQuery', {
defaultMessage: 'Good query',
})}
@ -98,7 +100,7 @@ export function CustomKqlIndicatorTypeForm() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="customKqlIndicatorFormTotalQueryInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.sliType.customKql.totalQuery', {
defaultMessage: 'Total query',
})}

View file

@ -23,7 +23,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';
export { NEW_CUSTOM_METRIC } from './metric_indicator';
@ -33,9 +33,11 @@ const SUPPORTED_METRIC_FIELD_TYPES = ['number', 'histogram'];
export function CustomMetricIndicatorTypeForm() {
const { watch } = useFormContext<CreateSLOForm>();
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() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="customMetricIndicatorFormQueryFilterInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.queryFilter', {
defaultMessage: 'Query filter',
})}
@ -120,6 +122,7 @@ export function CustomMetricIndicatorTypeForm() {
type="good"
metricFields={metricFields ?? []}
isLoadingIndex={isIndexFieldsLoading}
dataView={dataView}
/>
</EuiFlexItem>
@ -141,6 +144,7 @@ export function CustomMetricIndicatorTypeForm() {
type="total"
metricFields={metricFields ?? []}
isLoadingIndex={isIndexFieldsLoading}
dataView={dataView}
/>
</EuiFlexItem>

View file

@ -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<CreateSLOForm>();
const [options, setOptions] = useState<Option[]>(createOptionsFromFields(metricFields));
const [aggregationOptions, setAggregationOptions] = useState(CUSTOM_METRIC_AGGREGATION_OPTIONS);
@ -125,96 +132,29 @@ export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIn
return (
<>
<EuiFlexItem>
{fields?.map((metric, index) => (
<EuiFlexGroup alignItems="center" gutterSize="xs" key={metric.id}>
<input hidden {...register(`indicator.params.${type}.metrics.${index}.name`)} />
<EuiFlexItem>
<EuiFormRow
fullWidth
isInvalid={
getFieldState(`indicator.params.${type}.metrics.${index}.aggregation`).invalid
}
label={
<span>
{i18n.translate('xpack.slo.sloEdit.customMetric.aggregationLabel', {
defaultMessage: 'Aggregation',
})}{' '}
{metric.name}
</span>
}
>
<Controller
name={`indicator.params.${type}.metrics.${index}.aggregation`}
defaultValue="sum"
rules={{ required: true }}
control={control}
render={({ field: { ref, ...field }, fieldState }) => (
<EuiComboBox
{...field}
async
fullWidth
singleSelection={{ asPlainText: true }}
placeholder={i18n.translate(
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
{ defaultMessage: 'Select an aggregation' }
)}
aria-label={i18n.translate(
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
{ defaultMessage: 'Select an aggregation' }
)}
isClearable
isInvalid={fieldState.invalid}
isDisabled={isLoadingIndex || !indexPattern}
isLoading={!!indexPattern && isLoadingIndex}
onChange={(selected: EuiComboBoxOptionOption[]) => {
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}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
{watch(`indicator.params.${type}.metrics.${index}.aggregation`) !== 'doc_count' && (
{fields?.map((metric, index, arr) => (
<div key={metric.id}>
<EuiFlexGroup alignItems="center" gutterSize="xs" key={metric.id}>
<input hidden {...register(`indicator.params.${type}.metrics.${index}.name`)} />
<EuiFlexItem>
<EuiFormRow
fullWidth
isInvalid={
getFieldState(`indicator.params.${type}.metrics.${index}.field`).invalid
getFieldState(`indicator.params.${type}.metrics.${index}.aggregation`).invalid
}
label={
<span>
{metricLabel} {metric.name} {metricTooltip}
{i18n.translate('xpack.slo.sloEdit.customMetric.aggregationLabel', {
defaultMessage: 'Aggregation',
})}{' '}
{metric.name}
</span>
}
>
<Controller
name={`indicator.params.${type}.metrics.${index}.field`}
defaultValue=""
name={`indicator.params.${type}.metrics.${index}.aggregation`}
defaultValue="sum"
rules={{ required: true }}
shouldUnregister
control={control}
render={({ field: { ref, ...field }, fieldState }) => (
<EuiComboBox
@ -223,12 +163,12 @@ export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIn
fullWidth
singleSelection={{ asPlainText: true }}
placeholder={i18n.translate(
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
{ defaultMessage: 'Select an aggregation' }
)}
aria-label={i18n.translate(
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
'xpack.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
{ defaultMessage: 'Select an aggregation' }
)}
isClearable
isInvalid={fieldState.invalid}
@ -243,66 +183,134 @@ export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIn
selectedOptions={
!!indexPattern &&
!!field.value &&
metricFields.some((metricField) => 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}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
)}
<EuiFlexItem>
<QueryBuilder
dataTestSubj="customKqlIndicatorFormGoodQueryInput"
indexPatternString={watch('indicator.params.index')}
label={`${filterLabel} ${metric.name}`}
name={`indicator.params.${type}.metrics.${index}.filter`}
placeholder={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.placeholder', {
defaultMessage: 'KQL filter',
})}
required={false}
tooltip={
<EuiIconTip
content={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.tooltip', {
defaultMessage: 'This KQL query should return a subset of events.',
})}
position="top"
/>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={0}>
<EuiButtonIcon
data-test-subj="o11yMetricIndicatorButton"
iconType="trash"
color="danger"
style={{ marginTop: '1.5em' }}
onClick={handleDeleteMetric(index)}
disabled={disableDelete}
title={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.deleteLabel', {
defaultMessage: 'Delete metric',
})}
aria-label={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.deleteLabel', {
defaultMessage: 'Delete metric',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
{watch(`indicator.params.${type}.metrics.${index}.aggregation`) !== 'doc_count' && (
<EuiFlexItem>
<EuiFormRow
fullWidth
isInvalid={
getFieldState(`indicator.params.${type}.metrics.${index}.field`).invalid
}
label={
<span>
{metricLabel} {metric.name} {metricTooltip}
</span>
}
>
<Controller
name={`indicator.params.${type}.metrics.${index}.field`}
defaultValue=""
rules={{ required: true }}
shouldUnregister
control={control}
render={({ field: { ref, ...field }, fieldState }) => (
<EuiComboBox
{...field}
async
fullWidth
singleSelection={{ asPlainText: true }}
placeholder={i18n.translate(
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
)}
aria-label={i18n.translate(
'xpack.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
)}
isClearable
isInvalid={fieldState.invalid}
isDisabled={isLoadingIndex || !indexPattern}
isLoading={!!indexPattern && isLoadingIndex}
onChange={(selected: EuiComboBoxOptionOption[]) => {
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}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
)}
<EuiFlexItem grow={0}>
<EuiButtonIcon
data-test-subj="o11yMetricIndicatorButton"
iconType="trash"
color="danger"
style={{ marginTop: '1.5em' }}
onClick={handleDeleteMetric(index)}
disabled={disableDelete}
title={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.deleteLabel', {
defaultMessage: 'Delete metric',
})}
aria-label={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.deleteLabel', {
defaultMessage: 'Delete metric',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
<QueryBuilder
dataTestSubj="customKqlIndicatorFormGoodQueryInput"
dataView={dataView}
label={`${filterLabel} ${metric.name}`}
name={`indicator.params.${type}.metrics.${index}.filter`}
placeholder={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.placeholder', {
defaultMessage: 'KQL filter',
})}
required={false}
tooltip={
<EuiIconTip
content={i18n.translate('xpack.slo.sloEdit.sliType.customMetric.tooltip', {
defaultMessage: 'This KQL query should return a subset of events.',
})}
position="top"
/>
}
/>
{index !== arr.length - 1 && <EuiHorizontalRule size="quarter" />}
</div>
))}
<EuiFlexGroup>
<EuiFlexItem grow={0}>

View file

@ -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 = (
<EuiIconTip
content={i18n.translate('xpack.slo.sloEdit.sliType.histogram.aggregationTooltip', {
defaultMessage:
'The "value count" aggreation will return the total count for the histogram field. Range will return the count from the histogram field that is within the range defined below.',
'The "value count" aggregation will return the total count for the histogram field. Range will return the count from the histogram field that is within the range defined below.',
})}
position="top"
/>
@ -94,6 +95,7 @@ export function HistogramIndicator({
type,
histogramFields,
isLoadingIndex,
dataView,
}: HistogramIndicatorProps) {
const { control, watch, getFieldState } = useFormContext<CreateSLOForm>();
const [options, setOptions] = useState<Option[]>(createOptionsFromFields(histogramFields));
@ -282,7 +284,7 @@ export function HistogramIndicator({
<EuiFlexItem>
<QueryBuilder
dataTestSubj={`histogramIndicatorForm${type}QueryInput`}
indexPatternString={indexPattern}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.sliType.histogram.kqlFilterLabel', {
defaultMessage: 'KQL filter',
})}

View file

@ -23,15 +23,17 @@ 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 { HistogramIndicator } from './histogram_indicator';
export function HistogramIndicatorTypeForm() {
const { watch } = useFormContext<CreateSLOForm>();
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() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="histogramIndicatorFormQueryFilterInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.sliType.histogram.queryFilter', {
defaultMessage: 'Query filter',
})}

View file

@ -16,6 +16,8 @@ import {
import moment from 'moment';
import React, { useState } 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 { formatAllFilters } from '../../helpers/format_filters';
import { CreateSLOForm } from '../../types';
import { DataPreviewChart } from '../common/data_preview_chart';
@ -25,6 +27,7 @@ import { FieldSelector } from '../synthetics_common/field_selector';
export function SyntheticsAvailabilityIndicatorTypeForm() {
const { watch } = useFormContext<CreateSLOForm<SyntheticsAvailabilityIndicator>>();
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() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="syntheticsAvailabilityFilterInput"
indexPatternString={index}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.syntheticsAvailability.filter', {
defaultMessage: 'Query filter',
})}

View file

@ -12,17 +12,19 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiHorizontalRule,
EuiIconTip,
EuiSelect,
EuiSpacer,
EuiText,
} 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';
import React from 'react';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
import { QueryBuilder } from '../common/query_builder';
import { COMPARATOR_OPTIONS } from '../../constants';
import { CreateSLOForm } from '../../types';
import { MetricInput } from './metric_input';
@ -30,6 +32,7 @@ import { MetricInput } from './metric_input';
interface MetricIndicatorProps {
indexFields: FieldSpec[];
isLoadingIndex: boolean;
dataView?: DataView;
}
export const NEW_TIMESLICE_METRIC = { name: 'A', aggregation: 'avg' as const, field: '' };
@ -75,7 +78,7 @@ const thresholdTooltip = (
/>
);
export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicatorProps) {
export function MetricIndicator({ indexFields, isLoadingIndex, dataView }: MetricIndicatorProps) {
const { control, watch, setValue, register, getFieldState } = useFormContext<CreateSLOForm>();
const { fields, append, remove } = useFieldArray({
@ -112,7 +115,7 @@ export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicator
return (
<>
<EuiFlexItem>
{fields?.map((metric, index) => (
{fields?.map((metric, index, arr) => (
<React.Fragment key={metric.id}>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<input hidden {...register(`indicator.params.metric.metrics.${index}.name`)} />
@ -140,7 +143,29 @@ export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicator
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<QueryBuilder
dataTestSubj="timesliceMetricIndicatorFormMetricQueryInput"
dataView={dataView}
label={`${filterLabel} ${metric.name}`}
name={`indicator.params.metric.metrics.${index}.filter`}
placeholder={i18n.translate(
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.placeholder',
{ defaultMessage: 'KQL filter' }
)}
required={false}
tooltip={
<EuiIconTip
content={i18n.translate(
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.tooltip',
{
defaultMessage: 'This KQL query should return a subset of events.',
}
)}
position="top"
/>
}
/>
{index !== arr.length - 1 && <EuiHorizontalRule size="quarter" />}
</React.Fragment>
))}
<EuiFlexGroup>
@ -288,3 +313,7 @@ export function MetricIndicator({ indexFields, isLoadingIndex }: MetricIndicator
</>
);
}
const filterLabel = i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.filterLabel', {
defaultMessage: 'Filter',
});

View file

@ -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 = (
<EuiIconTip
content={i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.totalMetric.tooltip', {
@ -257,30 +252,6 @@ export function MetricInput({
/>
</EuiFlexItem>
)}
<EuiFlexItem>
<QueryBuilder
dataTestSubj="timesliceMetricIndicatorFormMetricQueryInput"
indexPatternString={watch('indicator.params.index')}
label={`${filterLabel} ${metric.name}`}
name={`indicator.params.metric.metrics.${index}.filter`}
placeholder={i18n.translate(
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.placeholder',
{ defaultMessage: 'KQL filter' }
)}
required={false}
tooltip={
<EuiIconTip
content={i18n.translate(
'xpack.slo.sloEdit.sliType.timesliceMetric.goodQuery.tooltip',
{
defaultMessage: 'This KQL query should return a subset of events.',
}
)}
position="top"
/>
}
/>
</EuiFlexItem>
</>
);
}

View file

@ -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<CreateSLOForm>();
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() {
<EuiFlexItem>
<QueryBuilder
dataTestSubj="timesliceMetricIndicatorFormQueryFilterInput"
indexPatternString={watch('indicator.params.index')}
dataView={dataView}
label={i18n.translate('xpack.slo.sloEdit.sliType.timesliceMetric.queryFilter', {
defaultMessage: 'Query filter',
})}
@ -118,6 +120,7 @@ export function TimesliceMetricIndicatorTypeForm() {
<MetricIndicator
indexFields={dataView?.fields ?? []}
isLoadingIndex={isIndexFieldsLoading}
dataView={dataView}
/>
</EuiFlexItem>

View file

@ -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<CreateSLOForm>,
dataView?: DataView
): string[] => {
const { watch } = useFormContext<CreateSLOForm>();
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 ?? [];
};

View file

@ -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'],

View file

@ -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(<div>Tags list</div>);
@ -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();

View file

@ -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<SloPluginSetup> {
...plugins,
core,
},
getDataViewsStart: async () => {
const [, pluginStart] = await core.getStartServices();
return pluginStart.dataViews;
},
getSpacesStart: async () => {
const [, pluginStart] = await core.getStartServices();
return pluginStart.spaces;

View file

@ -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<RulesClientApi>;
getRacClientWithRequest: (request: KibanaRequest) => Promise<AlertsClient>;
getDataViewsStart: () => Promise<DataViewsServerPluginStart>;
}
export function registerRoutes({ config, repository, core, logger, dependencies }: RegisterRoutes) {

View file

@ -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);
},
});

View file

@ -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<string, any>;
rollUpTransform: TransformPutTransformRequest;
@ -97,13 +97,13 @@ export class CreateSLO {
temporaryDoc: Record<string, any>;
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 {

View file

@ -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<string, unknown>;
}
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: {

View file

@ -23,7 +23,7 @@ export class DefaultSummaryTransformManager implements TransformManager {
) {}
async install(slo: SLODefinition): Promise<TransformId> {
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<TransformPutTransformRequest> {
return this.generator.generate(slo);
}

View file

@ -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);

View file

@ -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<TransformPutTransformRequest> {
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: [

View file

@ -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);

View file

@ -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<TransformPutTransformRequest> {
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: [

View file

@ -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',

View file

@ -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);

View file

@ -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<TransformPutTransformRequest> {
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),
],
},
},

View file

@ -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);

View file

@ -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<TransformPutTransformRequest> {
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: [

View file

@ -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);

View file

@ -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<TransformPutTransformRequest> {
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),
],
},
},

View file

@ -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);

View file

@ -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<TransformPutTransformRequest> {
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,

View file

@ -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);

View file

@ -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<TransformPutTransformRequest> {
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),
],
},
},

View file

@ -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<TransformPutTransformRequest>;
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<DataView | undefined> {
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'

View file

@ -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<IndicatorTypes, TransformGenerator> = {
'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<TransformPutTransformRequest> {
return {} as TransformPutTransformRequest;
}
}
class FailTransformGenerator extends TransformGenerator {
getTransformParams(slo: SLODefinition): TransformPutTransformRequest {
getTransformParams(
slo: SLODefinition,
spaceId: string,
dataViewService: DataViewsService
): Promise<TransformPutTransformRequest> {
throw new Error('Some error');
}
}

View file

@ -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<TransformId>;
inspect(slo: SLODefinition): TransformPutTransformRequest;
inspect(slo: SLODefinition): Promise<TransformPutTransformRequest>;
preview(transformId: TransformId): Promise<void>;
start(transformId: TransformId): Promise<void>;
stop(transformId: TransformId): Promise<void>;
@ -29,7 +30,8 @@ export class DefaultTransformManager implements TransformManager {
private generators: Record<IndicatorTypes, TransformGenerator>,
private esClient: ElasticsearchClient,
private logger: Logger,
private spaceId: string
private spaceId: string,
private dataViewService: DataViewsService
) {}
async install(slo: SLODefinition): Promise<TransformId> {
@ -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<TransformPutTransformRequest> {
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<void> {