diff --git a/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts index 08a3c0520ca18..ac7f1ce0659f5 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import type { CreateSLOInput, CreateSLOResponse, FindSLOResponse } from '@kbn/slo-schema'; import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; @@ -12,6 +13,8 @@ import { v1 as uuidv1 } from 'uuid'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +type ServerError = IHttpFetchError; + export function useCloneSlo() { const { http, @@ -21,7 +24,7 @@ export function useCloneSlo() { return useMutation< CreateSLOResponse, - string, + ServerError, { slo: CreateSLOInput; originalSloId?: string }, { previousData?: FindSLOResponse; queryKey?: QueryKey } >( @@ -58,16 +61,17 @@ export function useCloneSlo() { return { queryKey, previousData }; }, // If the mutation fails, use the context returned from onMutate to roll back - onError: (_err, { slo }, context) => { + onError: (error, { slo }, context) => { if (context?.previousData && context?.queryKey) { queryClient.setQueryData(context.queryKey, context.previousData); } - toasts.addDanger( - i18n.translate('xpack.observability.slo.clone.errorNotification', { + + toasts.addError(new Error(error.body?.message ?? error.message), { + title: i18n.translate('xpack.observability.slo.clone.errorNotification', { defaultMessage: 'Failed to clone {name}', values: { name: slo.name }, - }) - ); + }), + }); }, onSuccess: (_data, { slo }) => { toasts.addSuccess( diff --git a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts index ca2a0435b741f..a1a79d51f5af5 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { encode } from '@kbn/rison'; import type { CreateSLOInput, CreateSLOResponse, FindSLOResponse } from '@kbn/slo-schema'; @@ -14,6 +15,8 @@ import { paths } from '../../../common/locators/paths'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +type ServerError = IHttpFetchError; + export function useCreateSlo() { const { application: { navigateToUrl }, @@ -24,7 +27,7 @@ export function useCreateSlo() { return useMutation< CreateSLOResponse, - string, + ServerError, { slo: CreateSLOInput }, { previousData?: FindSLOResponse; queryKey?: QueryKey } >( @@ -72,7 +75,7 @@ export function useCreateSlo() { queryClient.setQueryData(context.queryKey, context.previousData); } - toasts.addError(new Error(String(error)), { + toasts.addError(new Error(error.body?.message ?? error.message), { title: i18n.translate('xpack.observability.slo.create.errorNotification', { defaultMessage: 'Something went wrong while creating {name}', values: { name: slo.name }, diff --git a/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts index 7add468442677..3e6e77551f11c 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts @@ -8,9 +8,12 @@ import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; import { i18n } from '@kbn/i18n'; import { FindSLOResponse } from '@kbn/slo-schema'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +type ServerError = IHttpFetchError; + export function useDeleteSlo() { const { http, @@ -20,7 +23,7 @@ export function useDeleteSlo() { return useMutation< string, - string, + ServerError, { id: string; name: string }, { previousData?: FindSLOResponse; queryKey?: QueryKey } >( @@ -56,17 +59,17 @@ export function useDeleteSlo() { return { previousData, queryKey }; }, // If the mutation fails, use the context returned from onMutate to roll back - onError: (_err, { name }, context) => { + onError: (error, { name }, context) => { if (context?.previousData && context?.queryKey) { queryClient.setQueryData(context.queryKey, context.previousData); } - toasts.addDanger( - i18n.translate('xpack.observability.slo.slo.delete.errorNotification', { + toasts.addError(new Error(error.body?.message ?? error.message), { + title: i18n.translate('xpack.observability.slo.slo.delete.errorNotification', { defaultMessage: 'Failed to delete {name}', values: { name }, - }) - ); + }), + }); }, onSuccess: (_data, { name }) => { toasts.addSuccess( diff --git a/x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts b/x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts index f9ebff032b5da..0dfe9cbebc82d 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_get_preview_data.ts @@ -42,7 +42,7 @@ export function useGetPreviewData(isValid: boolean, indicator: Indicator): UseGe } ); - return response; + return Array.isArray(response) ? response : []; }, retry: false, refetchOnWindowFocus: false, diff --git a/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts index c158fa3f31f5e..07f6991b9e82b 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts @@ -5,12 +5,15 @@ * 2.0. */ +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import type { FindSLOResponse, UpdateSLOInput, UpdateSLOResponse } from '@kbn/slo-schema'; import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +type ServerError = IHttpFetchError; + export function useUpdateSlo() { const { http, @@ -20,7 +23,7 @@ export function useUpdateSlo() { return useMutation< UpdateSLOResponse, - string, + ServerError, { sloId: string; slo: UpdateSLOInput }, { previousData?: FindSLOResponse; queryKey?: QueryKey } >( @@ -69,7 +72,7 @@ export function useUpdateSlo() { queryClient.setQueryData(context.queryKey, context.previousData); } - toasts.addError(new Error(String(error)), { + toasts.addError(new Error(error.body?.message ?? error.message), { title: i18n.translate('xpack.observability.slo.update.errorNotification', { defaultMessage: 'Something went wrong when updating {name}', values: { name }, diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index 9e1db8c019381..651a97d364439 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -21,7 +21,7 @@ export function SloList({ autoRefresh }: Props) { const [query, setQuery] = useState(''); const [sort, setSort] = useState('status'); - const { isInitialLoading, isLoading, isRefetching, isError, sloList, refetch } = useFetchSloList({ + const { isLoading, isRefetching, isError, sloList } = useFetchSloList({ page: activePage + 1, kqlQuery: query, sortBy: sort, @@ -38,34 +38,23 @@ export function SloList({ autoRefresh }: Props) { const handlePageClick = (pageNumber: number) => { setActivePage(pageNumber); - refetch(); }; const handleChangeQuery = (newQuery: string) => { setActivePage(0); setQuery(newQuery); - refetch(); }; const handleChangeSort = (newSort: SortField | undefined) => { setActivePage(0); setSort(newSort); - refetch(); }; return ( diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx index 8a9b22c6b5c7f..c1d9c6b3c3a07 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx @@ -16,6 +16,7 @@ import { EuiSelectableOption, } from '@elastic/eui'; import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; +import { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import React, { useState } from 'react'; @@ -103,9 +104,13 @@ export function SloListSearchFilterSortBar({ unifiedSearch, }} disableAutoFocus - onSubmit={() => onChangeQuery(query)} + onSubmit={(value: Query) => { + setQuery(String(value.query)); + onChangeQuery(String(value.query)); + }} disableLanguageSwitcher isDisabled={loading} + autoSubmit indexPatterns={dataView ? [dataView] : []} placeholder={i18n.translate('xpack.observability.slo.list.search', { defaultMessage: 'Search your SLOs...', diff --git a/x-pack/plugins/observability/server/assets/constants.ts b/x-pack/plugins/observability/server/assets/constants.ts index 04161ba930310..0dd9df915eee4 100644 --- a/x-pack/plugins/observability/server/assets/constants.ts +++ b/x-pack/plugins/observability/server/assets/constants.ts @@ -6,6 +6,7 @@ */ export const SLO_RESOURCES_VERSION = 2; +export const SLO_SUMMARY_TRANSFORMS_VERSION = 3; export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.sli-mappings'; export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.sli-settings'; diff --git a/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts index 8869780a969ce..aac6a7c2ddb8c 100644 --- a/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts +++ b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts @@ -36,6 +36,7 @@ export const getSLOTransformTemplate = ( dest: destination, settings: { deduce_mappings: false, + unattended: true, }, sync: { time: { diff --git a/x-pack/plugins/observability/server/errors/errors.ts b/x-pack/plugins/observability/server/errors/errors.ts index dbbb873925636..cbecb88d9ce05 100644 --- a/x-pack/plugins/observability/server/errors/errors.ts +++ b/x-pack/plugins/observability/server/errors/errors.ts @@ -25,3 +25,5 @@ export class InternalQueryError extends ObservabilityError {} export class NotSupportedError extends ObservabilityError {} export class IllegalArgumentError extends ObservabilityError {} export class InvalidTransformError extends ObservabilityError {} + +export class SecurityException extends ObservabilityError {} diff --git a/x-pack/plugins/observability/server/errors/handler.ts b/x-pack/plugins/observability/server/errors/handler.ts index 943983439ca60..2898e53624832 100644 --- a/x-pack/plugins/observability/server/errors/handler.ts +++ b/x-pack/plugins/observability/server/errors/handler.ts @@ -9,6 +9,7 @@ import { CompositeSLOIdConflict, CompositeSLONotFound, ObservabilityError, + SecurityException, SLOIdConflict, SLONotFound, } from './errors'; @@ -22,5 +23,9 @@ export function getHTTPResponseCode(error: ObservabilityError): number { return 409; } + if (error instanceof SecurityException) { + return 403; + } + return 400; } diff --git a/x-pack/plugins/observability/server/services/slo/create_slo.test.ts b/x-pack/plugins/observability/server/services/slo/create_slo.test.ts index 0e690027b93b6..bd34d652e5fa4 100644 --- a/x-pack/plugins/observability/server/services/slo/create_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/create_slo.test.ts @@ -55,6 +55,7 @@ describe('CreateSLO', () => { expect(mockTransformManager.install).toHaveBeenCalledWith( expect.objectContaining({ ...sloParams, id: 'unique-id' }) ); + expect(mockTransformManager.preview).toHaveBeenCalledWith('slo-transform-id'); expect(mockTransformManager.start).toHaveBeenCalledWith('slo-transform-id'); expect(response).toEqual(expect.objectContaining({ id: 'unique-id' })); expect(esClientMock.index.mock.calls[0]).toMatchSnapshot(); @@ -100,6 +101,16 @@ describe('CreateSLO', () => { expect(mockRepository.deleteById).toBeCalled(); }); + it('removes the transform and deletes the SLO when transform preview fails', async () => { + mockTransformManager.install.mockResolvedValue('slo-transform-id'); + mockTransformManager.preview.mockRejectedValue(new Error('Transform preview error')); + const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); + + await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform preview error'); + expect(mockTransformManager.uninstall).toBeCalledWith('slo-transform-id'); + expect(mockRepository.deleteById).toBeCalled(); + }); + it('removes the transform and deletes the SLO when transform start fails', async () => { mockTransformManager.install.mockResolvedValue('slo-transform-id'); mockTransformManager.start.mockRejectedValue(new Error('Transform start error')); diff --git a/x-pack/plugins/observability/server/services/slo/create_slo.ts b/x-pack/plugins/observability/server/services/slo/create_slo.ts index b7ed87aef9051..0f97e91a81a89 100644 --- a/x-pack/plugins/observability/server/services/slo/create_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/create_slo.ts @@ -36,6 +36,7 @@ export class CreateSLO { } try { + await this.transformManager.preview(sloTransformId); await this.transformManager.start(sloTransformId); } catch (err) { await Promise.all([ diff --git a/x-pack/plugins/observability/server/services/slo/get_preview_data.ts b/x-pack/plugins/observability/server/services/slo/get_preview_data.ts index e85085dcec115..98f07e1f8ed5e 100644 --- a/x-pack/plugins/observability/server/services/slo/get_preview_data.ts +++ b/x-pack/plugins/observability/server/services/slo/get_preview_data.ts @@ -16,12 +16,13 @@ import { KQLCustomIndicator, MetricCustomIndicator, } from '@kbn/slo-schema'; +import { assertNever } from '@kbn/std'; import { APMTransactionDurationIndicator } from '../../domain/models'; import { computeSLI } from '../../domain/services'; import { InvalidQueryError } from '../../errors'; import { - GetHistogramIndicatorAggregation, GetCustomMetricIndicatorAggregation, + GetHistogramIndicatorAggregation, } from './aggregations'; export class GetPreviewData { @@ -299,19 +300,24 @@ export class GetPreviewData { } public async execute(params: GetPreviewDataParams): Promise { - switch (params.indicator.type) { - case 'sli.apm.transactionDuration': - return this.getAPMTransactionDurationPreviewData(params.indicator); - case 'sli.apm.transactionErrorRate': - return this.getAPMTransactionErrorPreviewData(params.indicator); - case 'sli.kql.custom': - return this.getCustomKQLPreviewData(params.indicator); - case 'sli.histogram.custom': - return this.getHistogramPreviewData(params.indicator); - case 'sli.metric.custom': - return this.getCustomMetricPreviewData(params.indicator); - default: - return []; + try { + const type = params.indicator.type; + switch (type) { + case 'sli.apm.transactionDuration': + return this.getAPMTransactionDurationPreviewData(params.indicator); + case 'sli.apm.transactionErrorRate': + return this.getAPMTransactionErrorPreviewData(params.indicator); + case 'sli.kql.custom': + return this.getCustomKQLPreviewData(params.indicator); + case 'sli.histogram.custom': + return this.getHistogramPreviewData(params.indicator); + case 'sli.metric.custom': + return this.getCustomMetricPreviewData(params.indicator); + default: + assertNever(type); + } + } catch (err) { + return []; } } } diff --git a/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts b/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts index 2e582fd3eac84..41ff658c89de5 100644 --- a/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts +++ b/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts @@ -279,6 +279,7 @@ function generateSearchQuery( window: timeWindowDurationInDays * bucketsPerDay, shift: 1, script: 'MovingFunctions.sum(values)', + gap_policy: 'insert_zeros', }, }, cumulative_total: { @@ -287,6 +288,7 @@ function generateSearchQuery( window: timeWindowDurationInDays * bucketsPerDay, shift: 1, script: 'MovingFunctions.sum(values)', + gap_policy: 'insert_zeros', }, }, }, diff --git a/x-pack/plugins/observability/server/services/slo/mocks/index.ts b/x-pack/plugins/observability/server/services/slo/mocks/index.ts index 36afc3a9191cd..979c3057d1e8e 100644 --- a/x-pack/plugins/observability/server/services/slo/mocks/index.ts +++ b/x-pack/plugins/observability/server/services/slo/mocks/index.ts @@ -28,6 +28,7 @@ const createSummaryTransformInstallerMock = (): jest.Mocked => { return { install: jest.fn(), + preview: jest.fn(), uninstall: jest.fn(), start: jest.fn(), stop: jest.fn(), diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/__snapshots__/summary_transform_installer.test.ts.snap b/x-pack/plugins/observability/server/services/slo/summary_transform/__snapshots__/summary_transform_installer.test.ts.snap index 0a76a924129b2..5161c5e5cb51b 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/__snapshots__/summary_transform_installer.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/__snapshots__/summary_transform_installer.test.ts.snap @@ -7,7 +7,7 @@ Array [ "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, "description": "Summarize every SLO with timeslices budgeting method and a 7 days rolling time window", "dest": Object { @@ -173,6 +173,7 @@ Array [ }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": ".slo-observability.sli-v2*", @@ -235,7 +236,7 @@ Array [ "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, "description": "Summarize every SLO with timeslices budgeting method and a 30 days rolling time window", "dest": Object { @@ -401,6 +402,7 @@ Array [ }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": ".slo-observability.sli-v2*", @@ -463,7 +465,7 @@ Array [ "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, "description": "Summarize every SLO with timeslices budgeting method and a 90 days rolling time window", "dest": Object { @@ -629,6 +631,7 @@ Array [ }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": ".slo-observability.sli-v2*", @@ -691,7 +694,7 @@ Array [ "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, "description": "Summarize every SLO with timeslices budgeting method and a weekly calendar aligned time window", "dest": Object { @@ -870,6 +873,7 @@ Array [ }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": ".slo-observability.sli-v2*", @@ -932,7 +936,7 @@ Array [ "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, "description": "Summarize every SLO with timeslices budgeting method and a monthly calendar aligned time window", "dest": Object { @@ -1126,6 +1130,7 @@ Array [ }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": ".slo-observability.sli-v2*", diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.ts index 0fc7bb1b904d9..d1be4c0711e5d 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.ts @@ -8,7 +8,7 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { - SLO_RESOURCES_VERSION, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../assets/constants'; import { retryTransientEsErrors } from '../../../utils/retry'; @@ -32,7 +32,7 @@ export class DefaultSummaryTransformInstaller implements SummaryTransformInstall const alreadyInstalled = summaryTransforms.count === allTransformIds.length && summaryTransforms.transforms.every( - (transform) => transform._meta?.version === SLO_RESOURCES_VERSION + (transform) => transform._meta?.version === SLO_SUMMARY_TRANSFORMS_VERSION ) && summaryTransforms.transforms.every((transform) => allTransformIds.includes(transform.id)); @@ -46,9 +46,9 @@ export class DefaultSummaryTransformInstaller implements SummaryTransformInstall const transform = summaryTransforms.transforms.find((t) => t.id === transformId); const transformAlreadyInstalled = - !!transform && transform._meta?.version === SLO_RESOURCES_VERSION; + !!transform && transform._meta?.version === SLO_SUMMARY_TRANSFORMS_VERSION; const previousTransformAlreadyInstalled = - !!transform && transform._meta?.version !== SLO_RESOURCES_VERSION; + !!transform && transform._meta?.version !== SLO_SUMMARY_TRANSFORMS_VERSION; if (transformAlreadyInstalled) { this.logger.info(`SLO summary transform [${transformId}] already installed - skipping`); diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_30d_rolling.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_30d_rolling.ts index 4e1279392a0d1..4fea113a54233 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_30d_rolling.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_30d_rolling.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -143,10 +143,10 @@ export const SUMMARY_OCCURRENCES_30D_ROLLING: TransformPutTransformRequest = { }, settings: { deduce_mappings: false, - max_page_search_size: 8000, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_7d_rolling.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_7d_rolling.ts index fd04fa207744d..629f7b234843c 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_7d_rolling.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_7d_rolling.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -143,9 +143,10 @@ export const SUMMARY_OCCURRENCES_7D_ROLLING: TransformPutTransformRequest = { }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_90d_rolling.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_90d_rolling.ts index 2ec860a4cdbfb..dfd77783ef3d2 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_90d_rolling.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_90d_rolling.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -143,9 +143,10 @@ export const SUMMARY_OCCURRENCES_90D_ROLLING: TransformPutTransformRequest = { }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_monthly_aligned.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_monthly_aligned.ts index 083bdcb174542..9d07bcf9cd5af 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_monthly_aligned.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_monthly_aligned.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -141,9 +141,10 @@ export const SUMMARY_OCCURRENCES_MONTHLY_ALIGNED: TransformPutTransformRequest = }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_weekly_aligned.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_weekly_aligned.ts index 45dc45200906c..0fc3ec19d3cf2 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_weekly_aligned.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_weekly_aligned.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -141,9 +141,10 @@ export const SUMMARY_OCCURRENCES_WEEKLY_ALIGNED: TransformPutTransformRequest = }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_30d_rolling.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_30d_rolling.ts index ae5f0a36650d4..5f48a86d0bca8 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_30d_rolling.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_30d_rolling.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -143,9 +143,10 @@ export const SUMMARY_TIMESLICES_30D_ROLLING: TransformPutTransformRequest = { }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_7d_rolling.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_7d_rolling.ts index 3db1d5818d66e..7a491337fba65 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_7d_rolling.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_7d_rolling.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -143,9 +143,10 @@ export const SUMMARY_TIMESLICES_7D_ROLLING: TransformPutTransformRequest = { }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_90d_rolling.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_90d_rolling.ts index 089d155896f80..c26c9f84e554f 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_90d_rolling.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_90d_rolling.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -143,9 +143,10 @@ export const SUMMARY_TIMESLICES_90D_ROLLING: TransformPutTransformRequest = { }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_monthly_aligned.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_monthly_aligned.ts index 1d27c7c2f4a29..18fd40a729403 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_monthly_aligned.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_monthly_aligned.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -171,9 +171,10 @@ export const SUMMARY_TIMESLICES_MONTHLY_ALIGNED: TransformPutTransformRequest = }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_weekly_aligned.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_weekly_aligned.ts index 1b46171c2cf10..af062b7b6abe6 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_weekly_aligned.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_weekly_aligned.ts @@ -8,9 +8,9 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { SLO_DESTINATION_INDEX_PATTERN, - SLO_RESOURCES_VERSION, SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORMS_VERSION, SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../../../assets/constants'; import { groupBy } from './common'; @@ -156,9 +156,10 @@ export const SUMMARY_TIMESLICES_WEEKLY_ALIGNED: TransformPutTransformRequest = { }, settings: { deduce_mappings: false, + unattended: true, }, _meta: { - version: SLO_RESOURCES_VERSION, + version: SLO_SUMMARY_TRANSFORMS_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap index aea9ba75f2863..d5ac57c80e40d 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap @@ -739,6 +739,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": "metrics-apm*", @@ -1013,6 +1014,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": "metrics-apm*", diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap index b7c4ebf0f6b8c..c8d687383b513 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -708,6 +708,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": "metrics-apm*", @@ -971,6 +972,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": "metrics-apm*", diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/histogram.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/histogram.test.ts.snap index ad100c4662fe9..cdbdd8c527043 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/histogram.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/histogram.test.ts.snap @@ -220,6 +220,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": Array [ @@ -476,6 +477,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": Array [ diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap index 3297a8c513821..27da87629465d 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap @@ -235,6 +235,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": Array [ @@ -465,6 +466,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": Array [ diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap index 362b1d4a9e01e..7c1765953d154 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap @@ -244,6 +244,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": Array [ @@ -512,6 +513,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "unattended": true, }, "source": Object { "index": Array [ diff --git a/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts b/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts index 0b0074be5b266..b05feb8d9e8f4 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts @@ -47,7 +47,7 @@ describe('TransformManager', () => { await expect( service.install(createSLO({ indicator: createAPMTransactionErrorRateIndicator() })) - ).rejects.toThrowError('Unsupported SLI type: sli.apm.transactionErrorRate'); + ).rejects.toThrowError('Unsupported indicator type [sli.apm.transactionErrorRate]'); }); it('throws when transform generator fails', async () => { @@ -80,6 +80,20 @@ describe('TransformManager', () => { }); }); + describe('Preview', () => { + it('previews the transform', async () => { + // @ts-ignore defining only a subset of the possible SLI + const generators: Record = { + 'sli.apm.transactionErrorRate': new ApmTransactionErrorRateTransformGenerator(), + }; + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); + + await transformManager.preview('slo-transform-id'); + + expect(esClientMock.transform.previewTransform).toHaveBeenCalledTimes(1); + }); + }); + describe('Start', () => { it('starts the transform', async () => { // @ts-ignore defining only a subset of the possible SLI diff --git a/x-pack/plugins/observability/server/services/slo/transform_manager.ts b/x-pack/plugins/observability/server/services/slo/transform_manager.ts index 14b600a746cac..ed35512c03b65 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_manager.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_manager.ts @@ -8,6 +8,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { SLO, IndicatorTypes } from '../../domain/models'; +import { SecurityException } from '../../errors'; import { retryTransientEsErrors } from '../../utils/retry'; import { TransformGenerator } from './transform_generators'; @@ -15,6 +16,7 @@ type TransformId = string; export interface TransformManager { install(slo: SLO): Promise; + preview(transformId: TransformId): Promise; start(transformId: TransformId): Promise; stop(transformId: TransformId): Promise; uninstall(transformId: TransformId): Promise; @@ -30,8 +32,8 @@ export class DefaultTransformManager implements TransformManager { async install(slo: SLO): Promise { const generator = this.generators[slo.indicator.type]; if (!generator) { - this.logger.error(`No transform generator found for ${slo.indicator.type} SLO type`); - throw new Error(`Unsupported SLI type: ${slo.indicator.type}`); + this.logger.error(`No transform generator found for indicator type [${slo.indicator.type}]`); + throw new Error(`Unsupported indicator type [${slo.indicator.type}]`); } const transformParams = generator.getTransformParams(slo); @@ -40,13 +42,29 @@ export class DefaultTransformManager implements TransformManager { logger: this.logger, }); } catch (err) { - this.logger.error(`Cannot create transform for ${slo.indicator.type} SLI type: ${err}`); + this.logger.error(`Cannot create SLO transform for indicator type [${slo.indicator.type}]`); + if (err.meta?.body?.error?.type === 'security_exception') { + throw new SecurityException(err.meta.body.error.reason); + } + throw err; } return transformParams.transform_id; } + async preview(transformId: string): Promise { + try { + await retryTransientEsErrors( + () => this.esClient.transform.previewTransform({ transform_id: transformId }), + { logger: this.logger } + ); + } catch (err) { + this.logger.error(`Cannot preview SLO transform [${transformId}]`); + throw err; + } + } + async start(transformId: TransformId): Promise { try { await retryTransientEsErrors( @@ -55,7 +73,7 @@ export class DefaultTransformManager implements TransformManager { { logger: this.logger } ); } catch (err) { - this.logger.error(`Cannot start transform id ${transformId}: ${err}`); + this.logger.error(`Cannot start SLO transform [${transformId}]`); throw err; } } @@ -71,7 +89,7 @@ export class DefaultTransformManager implements TransformManager { { logger: this.logger } ); } catch (err) { - this.logger.error(`Cannot stop transform id ${transformId}: ${err}`); + this.logger.error(`Cannot stop SLO transform [${transformId}]`); throw err; } } @@ -87,7 +105,7 @@ export class DefaultTransformManager implements TransformManager { { logger: this.logger } ); } catch (err) { - this.logger.error(`Cannot delete transform id ${transformId}: ${err}`); + this.logger.error(`Cannot delete SLO transform [${transformId}]`); throw err; } } diff --git a/x-pack/plugins/observability/server/services/slo/update_slo.test.ts b/x-pack/plugins/observability/server/services/slo/update_slo.test.ts index d2e03fe97dde8..f5b6ec0e9bcb0 100644 --- a/x-pack/plugins/observability/server/services/slo/update_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/update_slo.test.ts @@ -45,7 +45,7 @@ describe('UpdateSLO', () => { const newSettings = { ...slo.settings, timestamp_field: 'newField' }; await updateSLO.execute(slo.id, { settings: newSettings }); - expectDeletionOfObsoleteSLOData(slo); + expectDeletionOfOriginalSLO(slo); expect(mockRepository.save).toBeCalledWith( expect.objectContaining({ ...slo, @@ -70,7 +70,7 @@ describe('UpdateSLO', () => { }, }); - expectDeletionOfObsoleteSLOData(slo); + expectDeletionOfOriginalSLO(slo); expectInstallationOfNewSLOTransform(); }); @@ -86,7 +86,7 @@ describe('UpdateSLO', () => { }, }); - expectDeletionOfObsoleteSLOData(slo); + expectDeletionOfOriginalSLO(slo); expectInstallationOfNewSLOTransform(); }); @@ -102,7 +102,7 @@ describe('UpdateSLO', () => { }, }); - expectDeletionOfObsoleteSLOData(slo); + expectDeletionOfOriginalSLO(slo); expectInstallationOfNewSLOTransform(); }); @@ -119,7 +119,7 @@ describe('UpdateSLO', () => { expect(mockEsClient.index.mock.calls[0]).toMatchSnapshot(); }); - it('removes the obsolete data from the SLO previous revision', async () => { + it('removes the original data from the original SLO', async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), }); @@ -128,7 +128,6 @@ describe('UpdateSLO', () => { const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); await updateSLO.execute(slo.id, { indicator: newIndicator }); - expectDeletionOfObsoleteSLOData(slo); expect(mockRepository.save).toBeCalledWith( expect.objectContaining({ ...slo, @@ -138,19 +137,67 @@ describe('UpdateSLO', () => { }) ); expectInstallationOfNewSLOTransform(); + expectDeletionOfOriginalSLO(slo); + }); + + describe('when error happens during the transform installation step', () => { + it('restores the previous SLO definition in the repository', async () => { + const slo = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), + }); + mockRepository.findById.mockResolvedValueOnce(slo); + mockTransformManager.install.mockRejectedValueOnce(new Error('Transform install error')); + + const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); + + await expect(updateSLO.execute(slo.id, { indicator: newIndicator })).rejects.toThrowError( + 'Transform install error' + ); + + expect(mockRepository.save).toHaveBeenCalledWith(slo); + expect(mockTransformManager.preview).not.toHaveBeenCalled(); + expect(mockTransformManager.start).not.toHaveBeenCalled(); + expect(mockTransformManager.stop).not.toHaveBeenCalled(); + expect(mockTransformManager.uninstall).not.toHaveBeenCalled(); + expect(mockEsClient.deleteByQuery).not.toHaveBeenCalled(); + }); + }); + + describe('when error happens during the transform start step', () => { + it('removes the new transform and restores the previous SLO definition in the repository', async () => { + const slo = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), + }); + mockRepository.findById.mockResolvedValueOnce(slo); + mockTransformManager.start.mockRejectedValueOnce(new Error('Transform start error')); + + const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); + + await expect(updateSLO.execute(slo.id, { indicator: newIndicator })).rejects.toThrowError( + 'Transform start error' + ); + + expect(mockTransformManager.uninstall).toHaveBeenCalledWith( + getSLOTransformId(slo.id, slo.revision + 1) + ); + expect(mockRepository.save).toHaveBeenCalledWith(slo); + expect(mockTransformManager.stop).not.toHaveBeenCalled(); + expect(mockEsClient.deleteByQuery).not.toHaveBeenCalled(); + }); }); function expectInstallationOfNewSLOTransform() { - expect(mockTransformManager.start).toBeCalled(); expect(mockTransformManager.install).toBeCalled(); + expect(mockTransformManager.preview).toBeCalled(); + expect(mockTransformManager.start).toBeCalled(); } - function expectDeletionOfObsoleteSLOData(originalSlo: SLO) { + function expectDeletionOfOriginalSLO(originalSlo: SLO) { const transformId = getSLOTransformId(originalSlo.id, originalSlo.revision); expect(mockTransformManager.stop).toBeCalledWith(transformId); expect(mockTransformManager.uninstall).toBeCalledWith(transformId); - expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2); + expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2); expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith( 1, expect.objectContaining({ diff --git a/x-pack/plugins/observability/server/services/slo/update_slo.ts b/x-pack/plugins/observability/server/services/slo/update_slo.ts index 5bc10a7209106..bfebf8a5b392c 100644 --- a/x-pack/plugins/observability/server/services/slo/update_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/update_slo.ts @@ -36,10 +36,27 @@ export class UpdateSLO { validateSLO(updatedSlo); - await this.deleteObsoleteSLORevisionData(originalSlo); + const updatedSloTransformId = getSLOTransformId(updatedSlo.id, updatedSlo.revision); await this.repository.save(updatedSlo); - await this.transformManager.install(updatedSlo); - await this.transformManager.start(getSLOTransformId(updatedSlo.id, updatedSlo.revision)); + + try { + await this.transformManager.install(updatedSlo); + } catch (err) { + await this.repository.save(originalSlo); + throw err; + } + + try { + await this.transformManager.preview(updatedSloTransformId); + await this.transformManager.start(updatedSloTransformId); + } catch (err) { + await Promise.all([ + this.transformManager.uninstall(updatedSloTransformId), + this.repository.save(originalSlo), + ]); + + throw err; + } await this.esClient.index({ index: SLO_SUMMARY_TEMP_INDEX_NAME, @@ -47,13 +64,21 @@ export class UpdateSLO { document: createTempSummaryDocument(updatedSlo), }); + await this.deleteOriginalSLO(originalSlo); + return this.toResponse(updatedSlo); } - private async deleteObsoleteSLORevisionData(originalSlo: SLO) { - const originalSloTransformId = getSLOTransformId(originalSlo.id, originalSlo.revision); - await this.transformManager.stop(originalSloTransformId); - await this.transformManager.uninstall(originalSloTransformId); + private async deleteOriginalSLO(originalSlo: SLO) { + try { + const originalSloTransformId = getSLOTransformId(originalSlo.id, originalSlo.revision); + await this.transformManager.stop(originalSloTransformId); + await this.transformManager.uninstall(originalSloTransformId); + } catch (err) { + // Any errors here should not prevent moving forward. + // Worst case we keep rolling up data for the previous revision number. + } + await this.deleteRollupData(originalSlo.id, originalSlo.revision); await this.deleteSummaryData(originalSlo.id, originalSlo.revision); }