mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
217e29dc2a
commit
f3fdb0f398
53 changed files with 1017 additions and 482 deletions
|
@ -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,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"dataViews",
|
||||
"lens",
|
||||
"dataViewEditor",
|
||||
"dataViewFieldEditor",
|
||||
"fieldFormats",
|
||||
"observability",
|
||||
"observabilityShared",
|
||||
|
|
|
@ -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) };
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 ?? [];
|
||||
};
|
|
@ -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'],
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue