diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index e2f6f916b3ddac..7f7c317f7f63a1 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -721,7 +721,8 @@ "indicator.params", "indicator.type", "name", - "tags" + "tags", + "version" ], "threshold-explorer-view": [], "observability-onboarding-state": [ diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 8cedbf87227725..fe4b3dba0940d1 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -2379,6 +2379,9 @@ }, "tags": { "type": "keyword" + }, + "version": { + "type": "long" } } }, diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index f5127f499644e3..a2235b0f778120 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -142,7 +142,7 @@ describe('checking migration metadata changes on all registered SO types', () => "siem-ui-timeline": "d3de8ff3617be8f2a799d66b1471b9be6124bf40", "siem-ui-timeline-note": "0a32fb776907f596bedca292b8c646496ae9c57b", "siem-ui-timeline-pinned-event": "082daa3ce647b33873f6abccf340bdfa32057c8d", - "slo": "2048ab6791df2e1ae0936f29c20765cb8d2fcfaa", + "slo": "9a9995e4572de1839651c43b5fc4dc8276bb5815", "space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e", "spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e", "synthetics-monitor": "33ddc4b8979f378edf58bcc7ba13c5c5b572f42d", diff --git a/x-pack/packages/kbn-slo-schema/index.ts b/x-pack/packages/kbn-slo-schema/index.ts index 3d9e295055a618..98b183d391bb76 100644 --- a/x-pack/packages/kbn-slo-schema/index.ts +++ b/x-pack/packages/kbn-slo-schema/index.ts @@ -8,3 +8,4 @@ export * from './src/schema'; export * from './src/rest_specs'; export * from './src/models/duration'; +export * from './src/models/pagination'; diff --git a/x-pack/packages/kbn-slo-schema/src/models/pagination.ts b/x-pack/packages/kbn-slo-schema/src/models/pagination.ts new file mode 100644 index 00000000000000..815c30f71d4c40 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/models/pagination.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ +export interface Paginated { + total: number; + page: number; + perPage: number; + results: T[]; +} + +export interface Pagination { + page: number; + perPage: number; +} diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts index 54e463adc210ca..574a7eb1f9244f 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts @@ -6,6 +6,7 @@ */ import * as t from 'io-ts'; +import { toBooleanRt } from '@kbn/io-ts-utils'; import { allOrAnyString, apmTransactionDurationIndicatorSchema, @@ -109,6 +110,7 @@ const sloResponseSchema = t.intersection([ groupBy: allOrAnyString, createdAt: dateType, updatedAt: dateType, + version: t.number, }), t.partial({ instanceId: allOrAnyString, @@ -157,6 +159,12 @@ const manageSLOParamsSchema = t.type({ path: t.type({ id: sloIdSchema }), }); +const resetSLOParamsSchema = t.type({ + path: t.type({ id: sloIdSchema }), +}); + +const resetSLOResponseSchema = sloResponseSchema; + const updateSLOResponseSchema = sloResponseSchema; const findSLOResponseSchema = t.type({ @@ -182,23 +190,21 @@ const fetchHistoricalSummaryResponseSchema = t.array( }) ); -/** - * The query params schema for /internal/observability/slo/_definitions - * - * @private - */ -const findSloDefinitionsParamsSchema = t.type({ - query: t.type({ +const findSloDefinitionsParamsSchema = t.partial({ + query: t.partial({ search: t.string, + includeOutdatedOnly: toBooleanRt, + page: t.string, + perPage: t.string, }), }); -/** - * The response schema for /internal/observability/slo/_definitions - * - * @private - */ -const findSloDefinitionsResponseSchema = t.array(sloResponseSchema); +const findSloDefinitionsResponseSchema = t.type({ + page: t.number, + perPage: t.number, + total: t.number, + results: t.array(sloResponseSchema), +}); const getSLOBurnRatesResponseSchema = t.type({ burnRates: t.array( @@ -244,6 +250,9 @@ type GetSLOResponse = t.OutputOf; type ManageSLOParams = t.TypeOf; +type ResetSLOParams = t.TypeOf; +type ResetSLOResponse = t.OutputOf; + type UpdateSLOInput = t.OutputOf; type UpdateSLOParams = t.TypeOf; type UpdateSLOResponse = t.OutputOf; @@ -258,12 +267,8 @@ type FetchHistoricalSummaryParams = t.TypeOf; type HistoricalSummaryResponse = t.OutputOf; -/** - * The response type for /internal/observability/slo/_definitions - * - * @private - */ -type FindSloDefinitionsResponse = t.OutputOf; +type FindSLODefinitionsParams = t.TypeOf; +type FindSLODefinitionsResponse = t.OutputOf; type GetPreviewDataParams = t.TypeOf; type GetPreviewDataResponse = t.OutputOf; @@ -300,6 +305,8 @@ export { findSloDefinitionsParamsSchema, findSloDefinitionsResponseSchema, manageSLOParamsSchema, + resetSLOParamsSchema, + resetSLOResponseSchema, sloResponseSchema, sloWithSummaryResponseSchema, updateSLOParamsSchema, @@ -325,8 +332,11 @@ export type { FetchHistoricalSummaryParams, FetchHistoricalSummaryResponse, HistoricalSummaryResponse, - FindSloDefinitionsResponse, + FindSLODefinitionsParams, + FindSLODefinitionsResponse, ManageSLOParams, + ResetSLOParams, + ResetSLOResponse, SLOResponse, SLOWithSummaryResponse, UpdateSLOInput, diff --git a/x-pack/packages/kbn-slo-schema/src/schema/slo.ts b/x-pack/packages/kbn-slo-schema/src/schema/slo.ts index 29df82010710dc..27e8a9e998f718 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/slo.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/slo.ts @@ -50,6 +50,7 @@ const sloSchema = t.type({ createdAt: dateType, updatedAt: dateType, groupBy: allOrAnyString, + version: t.number, }); const sloWithSummarySchema = t.intersection([sloSchema, t.type({ summary: summarySchema })]); diff --git a/x-pack/packages/kbn-slo-schema/tsconfig.json b/x-pack/packages/kbn-slo-schema/tsconfig.json index 3c94d9a902af8d..bc9fd2fdede8a2 100644 --- a/x-pack/packages/kbn-slo-schema/tsconfig.json +++ b/x-pack/packages/kbn-slo-schema/tsconfig.json @@ -11,7 +11,8 @@ "**/*.ts" ], "kbn_references": [ - "@kbn/std" + "@kbn/std", + "@kbn/io-ts-utils" ], "exclude": [ "target/**/*", diff --git a/x-pack/performance/journeys/many_fields_transform.ts b/x-pack/performance/journeys/many_fields_transform.ts index 14187c20e5c594..6fe945914c3587 100644 --- a/x-pack/performance/journeys/many_fields_transform.ts +++ b/x-pack/performance/journeys/many_fields_transform.ts @@ -15,11 +15,11 @@ export const journey = new Journey({ .step('Go to Transforms', async ({ page, kbnUrl, kibanaPage }) => { await page.goto(kbnUrl.get(`app/management/data/transform`)); await kibanaPage.waitForHeader(); - await page.waitForSelector(subj('transformButtonCreate')); + await page.waitForSelector(subj('transformCreateFirstButton')); await page.waitForSelector(subj('globalLoadingIndicator-hidden')); }) .step('Go to data view selection', async ({ page }) => { - const createButtons = page.locator(subj('transformButtonCreate')); + const createButtons = page.locator(subj('transformCreateFirstButton')); await createButtons.first().click(); await page.waitForSelector(subj('savedObjectsFinderTable')); }) diff --git a/x-pack/plugins/observability/common/slo/constants.ts b/x-pack/plugins/observability/common/slo/constants.ts index 0dd9df915eee43..c2de5598fa8ce0 100644 --- a/x-pack/plugins/observability/common/slo/constants.ts +++ b/x-pack/plugins/observability/common/slo/constants.ts @@ -5,8 +5,8 @@ * 2.0. */ -export const SLO_RESOURCES_VERSION = 2; -export const SLO_SUMMARY_TRANSFORMS_VERSION = 3; +export const SLO_MODEL_VERSION = 2; +export const SLO_RESOURCES_VERSION = 3; export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.sli-mappings'; export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.sli-settings'; @@ -17,8 +17,7 @@ export const SLO_INDEX_TEMPLATE_PATTERN = `.slo-observability.sli-*`; export const SLO_DESTINATION_INDEX_NAME = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}`; export const SLO_DESTINATION_INDEX_PATTERN = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}*`; -export const SLO_INGEST_PIPELINE_NAME = `.slo-observability.sli.pipeline`; -// slo-observability.sli-v.(YYYY-MM-DD) +export const SLO_INGEST_PIPELINE_NAME = `.slo-observability.sli.pipeline-v${SLO_RESOURCES_VERSION}`; export const SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}.`; export const SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.summary-mappings'; @@ -26,12 +25,15 @@ export const SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability. export const SLO_SUMMARY_INDEX_TEMPLATE_NAME = '.slo-observability.summary'; export const SLO_SUMMARY_INDEX_TEMPLATE_PATTERN = `.slo-observability.summary-*`; -export const SLO_SUMMARY_TRANSFORM_NAME_PREFIX = 'slo-summary-'; export const SLO_SUMMARY_DESTINATION_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}`; // store the temporary summary document generated by transform export const SLO_SUMMARY_TEMP_INDEX_NAME = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}.temp`; // store the temporary summary document export const SLO_SUMMARY_DESTINATION_INDEX_PATTERN = `.slo-observability.summary-v${SLO_RESOURCES_VERSION}*`; // include temp and non-temp summary indices -export const SLO_SUMMARY_INGEST_PIPELINE_NAME = `.slo-observability.summary.pipeline`; - export const getSLOTransformId = (sloId: string, sloRevision: number) => `slo-${sloId}-${sloRevision}`; + +export const getSLOSummaryTransformId = (sloId: string, sloRevision: number) => + `slo-summary-${sloId}-${sloRevision}`; + +export const getSLOSummaryPipelineId = (sloId: string, sloRevision: number) => + `.slo-observability.summary.pipeline-${sloId}-${sloRevision}`; diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.json b/x-pack/plugins/observability/docs/openapi/slo/bundled.json index ff366afc2ff1f1..1ec1b5a629f022 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.json +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.json @@ -143,7 +143,7 @@ { "name": "page", "in": "query", - "description": "The page number to return", + "description": "The page to use for pagination, must be greater or equal than 1", "schema": { "type": "integer", "default": 1 @@ -153,7 +153,7 @@ { "name": "perPage", "in": "query", - "description": "The number of SLOs to return per page", + "description": "Number of SLOs returned by page", "schema": { "type": "integer", "default": 25, @@ -280,7 +280,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/slo_response" + "$ref": "#/components/schemas/slo_with_summary_response" } } } @@ -361,7 +361,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/slo_response" + "$ref": "#/components/schemas/slo_definition_response" } } } @@ -605,6 +605,79 @@ } } }, + "/s/{spaceId}/api/observability/slos/{sloId}/_reset": { + "post": { + "summary": "Resets an SLO.", + "operationId": "resetSloOp", + "description": "You must have the `write` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.\n", + "tags": [ + "slo" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "$ref": "#/components/parameters/slo_id" + } + ], + "responses": { + "204": { + "description": "Successful request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/slo_definition_response" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/400_response" + } + } + } + }, + "401": { + "description": "Unauthorized response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + }, + "403": { + "description": "Unauthorized response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/403_response" + } + } + } + }, + "404": { + "description": "Not found response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404_response" + } + } + } + } + } + } + }, "/s/{spaceId}/internal/observability/slos/_historical_summary": { "post": { "summary": "Retrieves the historical summary for a list of SLOs", @@ -675,6 +748,104 @@ } } }, + "/s/{spaceId}/internal/observability/slos/_definitions": { + "get": { + "summary": "Get the SLO definitions", + "operationId": "getDefinitionsOp", + "description": "You must have the `read` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.\n", + "tags": [ + "slo" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "includeOutdatedOnly", + "in": "query", + "description": "Indicates if the API returns only outdated SLO or all SLO definitions", + "schema": { + "type": "boolean" + }, + "example": true + }, + { + "name": "search", + "in": "query", + "description": "Filters the SLOs by name", + "schema": { + "type": "string" + }, + "example": "my service availability" + }, + { + "name": "page", + "in": "query", + "description": "The page to use for pagination, must be greater or equal than 1", + "schema": { + "type": "number" + }, + "example": 1 + }, + { + "name": "perPage", + "in": "query", + "description": "Number of SLOs returned by page", + "schema": { + "type": "integer", + "default": 100, + "maximum": 1000 + }, + "example": 100 + } + ], + "responses": { + "200": { + "description": "Successful request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/find_slo_definitions_response" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/400_response" + } + } + } + }, + "401": { + "description": "Unauthorized response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + }, + "403": { + "description": "Unauthorized response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/403_response" + } + } + } + } + } + } + }, "/s/{spaceId}/api/observability/slos/_delete_instances": { "post": { "summary": "Batch delete rollup and summary data for the matching list of sloId and instanceId", @@ -1587,7 +1758,7 @@ } } }, - "slo_response": { + "slo_with_summary_response": { "title": "SLO response", "type": "object", "required": [ @@ -1606,7 +1777,8 @@ "instanceId", "tags", "createdAt", - "updatedAt" + "updatedAt", + "version" ], "properties": { "id": { @@ -1708,6 +1880,11 @@ "description": "The last update date", "type": "string", "example": "2023-01-12T10:03:19.000Z" + }, + "version": { + "description": "The internal SLO version", + "type": "number", + "example": 2 } } }, @@ -1731,7 +1908,7 @@ "results": { "type": "array", "items": { - "$ref": "#/components/schemas/slo_response" + "$ref": "#/components/schemas/slo_with_summary_response" } } } @@ -1994,6 +2171,126 @@ } } }, + "slo_definition_response": { + "title": "SLO definition response", + "type": "object", + "required": [ + "id", + "name", + "description", + "indicator", + "timeWindow", + "budgetingMethod", + "objective", + "settings", + "revision", + "enabled", + "groupBy", + "tags", + "createdAt", + "updatedAt", + "version" + ], + "properties": { + "id": { + "description": "The identifier of the SLO.", + "type": "string", + "example": "8853df00-ae2e-11ed-90af-09bb6422b258" + }, + "name": { + "description": "The name of the SLO.", + "type": "string", + "example": "My Service SLO" + }, + "description": { + "description": "The description of the SLO.", + "type": "string", + "example": "My SLO description" + }, + "indicator": { + "discriminator": { + "propertyName": "type", + "mapping": { + "sli.apm.transactionErrorRate": "#/components/schemas/indicator_properties_apm_availability", + "sli.kql.custom": "#/components/schemas/indicator_properties_custom_kql", + "sli.apm.transactionDuration": "#/components/schemas/indicator_properties_apm_latency", + "sli.metric.custom": "#/components/schemas/indicator_properties_custom_metric", + "sli.histogram.custom": "#/components/schemas/indicator_properties_histogram", + "sli.metric.timeslice": "#/components/schemas/indicator_properties_timeslice_metric" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/indicator_properties_custom_kql" + }, + { + "$ref": "#/components/schemas/indicator_properties_apm_availability" + }, + { + "$ref": "#/components/schemas/indicator_properties_apm_latency" + }, + { + "$ref": "#/components/schemas/indicator_properties_custom_metric" + }, + { + "$ref": "#/components/schemas/indicator_properties_histogram" + }, + { + "$ref": "#/components/schemas/indicator_properties_timeslice_metric" + } + ] + }, + "timeWindow": { + "$ref": "#/components/schemas/time_window" + }, + "budgetingMethod": { + "$ref": "#/components/schemas/budgeting_method" + }, + "objective": { + "$ref": "#/components/schemas/objective" + }, + "settings": { + "$ref": "#/components/schemas/settings" + }, + "revision": { + "description": "The SLO revision", + "type": "number", + "example": 2 + }, + "enabled": { + "description": "Indicate if the SLO is enabled", + "type": "boolean", + "example": true + }, + "groupBy": { + "description": "optional group by field to use to generate an SLO per distinct value", + "type": "string", + "example": "some.field" + }, + "tags": { + "description": "List of tags", + "type": "array", + "items": { + "type": "string" + } + }, + "createdAt": { + "description": "The creation date", + "type": "string", + "example": "2023-01-12T10:03:19.000Z" + }, + "updatedAt": { + "description": "The last update date", + "type": "string", + "example": "2023-01-12T10:03:19.000Z" + }, + "version": { + "description": "The internal SLO version", + "type": "number", + "example": 2 + } + } + }, "historical_summary_request": { "title": "Historical summary request", "type": "object", @@ -2037,6 +2334,31 @@ } } }, + "find_slo_definitions_response": { + "title": "Find SLO definitions response", + "description": "A paginated response of SLO definitions matching the query.\n", + "type": "object", + "properties": { + "page": { + "type": "number", + "example": 2 + }, + "perPage": { + "type": "number", + "example": 100 + }, + "total": { + "type": "number", + "example": 123 + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/slo_definition_response" + } + } + } + }, "delete_slo_instances_request": { "title": "Delete SLO instances request", "description": "The delete SLO instances request takes a list of SLO id and instance id, then delete the rollup and summary data. This API can be used to remove the staled data of an instance SLO that no longer get updated.\n", diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml index 5aa20726b6a07d..643b0d29fea66d 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml @@ -86,14 +86,14 @@ paths: example: 'slo.name:latency* and slo.tags : "prod"' - name: page in: query - description: The page number to return + description: The page to use for pagination, must be greater or equal than 1 schema: type: integer default: 1 example: 1 - name: perPage in: query - description: The number of SLOs to return per page + description: Number of SLOs returned by page schema: type: integer default: 25 @@ -176,7 +176,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/slo_response' + $ref: '#/components/schemas/slo_with_summary_response' '400': description: Bad request content: @@ -224,7 +224,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/slo_response' + $ref: '#/components/schemas/slo_definition_response' '400': description: Bad request content: @@ -365,6 +365,49 @@ paths: application/json: schema: $ref: '#/components/schemas/404_response' + /s/{spaceId}/api/observability/slos/{sloId}/_reset: + post: + summary: Resets an SLO. + operationId: resetSloOp + description: | + You must have the `write` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges. + tags: + - slo + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - $ref: '#/components/parameters/slo_id' + responses: + '204': + description: Successful request + content: + application/json: + schema: + $ref: '#/components/schemas/slo_definition_response' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/400_response' + '401': + description: Unauthorized response + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + '403': + description: Unauthorized response + content: + application/json: + schema: + $ref: '#/components/schemas/403_response' + '404': + description: Not found response + content: + application/json: + schema: + $ref: '#/components/schemas/404_response' /s/{spaceId}/internal/observability/slos/_historical_summary: post: summary: Retrieves the historical summary for a list of SLOs @@ -407,6 +450,68 @@ paths: application/json: schema: $ref: '#/components/schemas/403_response' + /s/{spaceId}/internal/observability/slos/_definitions: + get: + summary: Get the SLO definitions + operationId: getDefinitionsOp + description: | + You must have the `read` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges. + tags: + - slo + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - name: includeOutdatedOnly + in: query + description: Indicates if the API returns only outdated SLO or all SLO definitions + schema: + type: boolean + example: true + - name: search + in: query + description: Filters the SLOs by name + schema: + type: string + example: my service availability + - name: page + in: query + description: The page to use for pagination, must be greater or equal than 1 + schema: + type: number + example: 1 + - name: perPage + in: query + description: Number of SLOs returned by page + schema: + type: integer + default: 100 + maximum: 1000 + example: 100 + responses: + '200': + description: Successful request + content: + application/json: + schema: + $ref: '#/components/schemas/find_slo_definitions_response' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/400_response' + '401': + description: Unauthorized response + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + '403': + description: Unauthorized response + content: + application/json: + schema: + $ref: '#/components/schemas/403_response' /s/{spaceId}/api/observability/slos/_delete_instances: post: summary: Batch delete rollup and summary data for the matching list of sloId and instanceId @@ -1103,7 +1208,7 @@ components: example: 0.9836 errorBudget: $ref: '#/components/schemas/error_budget' - slo_response: + slo_with_summary_response: title: SLO response type: object required: @@ -1123,6 +1228,7 @@ components: - tags - createdAt - updatedAt + - version properties: id: description: The identifier of the SLO. @@ -1192,6 +1298,10 @@ components: description: The last update date type: string example: '2023-01-12T10:03:19.000Z' + version: + description: The internal SLO version + type: number + example: 2 find_slo_response: title: Find SLO response description: | @@ -1210,7 +1320,7 @@ components: results: type: array items: - $ref: '#/components/schemas/slo_response' + $ref: '#/components/schemas/slo_with_summary_response' 400_response: title: Bad request type: object @@ -1386,6 +1496,92 @@ components: type: array items: type: string + slo_definition_response: + title: SLO definition response + type: object + required: + - id + - name + - description + - indicator + - timeWindow + - budgetingMethod + - objective + - settings + - revision + - enabled + - groupBy + - tags + - createdAt + - updatedAt + - version + properties: + id: + description: The identifier of the SLO. + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + name: + description: The name of the SLO. + type: string + example: My Service SLO + description: + description: The description of the SLO. + type: string + example: My SLO description + indicator: + discriminator: + propertyName: type + mapping: + sli.apm.transactionErrorRate: '#/components/schemas/indicator_properties_apm_availability' + sli.kql.custom: '#/components/schemas/indicator_properties_custom_kql' + sli.apm.transactionDuration: '#/components/schemas/indicator_properties_apm_latency' + sli.metric.custom: '#/components/schemas/indicator_properties_custom_metric' + sli.histogram.custom: '#/components/schemas/indicator_properties_histogram' + sli.metric.timeslice: '#/components/schemas/indicator_properties_timeslice_metric' + oneOf: + - $ref: '#/components/schemas/indicator_properties_custom_kql' + - $ref: '#/components/schemas/indicator_properties_apm_availability' + - $ref: '#/components/schemas/indicator_properties_apm_latency' + - $ref: '#/components/schemas/indicator_properties_custom_metric' + - $ref: '#/components/schemas/indicator_properties_histogram' + - $ref: '#/components/schemas/indicator_properties_timeslice_metric' + timeWindow: + $ref: '#/components/schemas/time_window' + budgetingMethod: + $ref: '#/components/schemas/budgeting_method' + objective: + $ref: '#/components/schemas/objective' + settings: + $ref: '#/components/schemas/settings' + revision: + description: The SLO revision + type: number + example: 2 + enabled: + description: Indicate if the SLO is enabled + type: boolean + example: true + groupBy: + description: optional group by field to use to generate an SLO per distinct value + type: string + example: some.field + tags: + description: List of tags + type: array + items: + type: string + createdAt: + description: The creation date + type: string + example: '2023-01-12T10:03:19.000Z' + updatedAt: + description: The last update date + type: string + example: '2023-01-12T10:03:19.000Z' + version: + description: The internal SLO version + type: number + example: 2 historical_summary_request: title: Historical summary request type: object @@ -1416,6 +1612,25 @@ components: example: 0.9836 errorBudget: $ref: '#/components/schemas/error_budget' + find_slo_definitions_response: + title: Find SLO definitions response + description: | + A paginated response of SLO definitions matching the query. + type: object + properties: + page: + type: number + example: 2 + perPage: + type: number + example: 100 + total: + type: number + example: 123 + results: + type: array + items: + $ref: '#/components/schemas/slo_definition_response' delete_slo_instances_request: title: Delete SLO instances request description: | diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/find_slo_definitions_response.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/find_slo_definitions_response.yaml new file mode 100644 index 00000000000000..274bdc7016a045 --- /dev/null +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/find_slo_definitions_response.yaml @@ -0,0 +1,18 @@ +title: Find SLO definitions response +description: > + A paginated response of SLO definitions matching the query. +type: object +properties: + page: + type: number + example: 2 + perPage: + type: number + example: 100 + total: + type: number + example: 123 + results: + type: array + items: + $ref: 'slo_definition_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/find_slo_response.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/find_slo_response.yaml index 36a701efa34f4a..b94aa6e6dc1c57 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/find_slo_response.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/find_slo_response.yaml @@ -15,4 +15,4 @@ properties: results: type: array items: - $ref: 'slo_response.yaml' \ No newline at end of file + $ref: 'slo_with_summary_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_definition_response.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_definition_response.yaml new file mode 100644 index 00000000000000..0b4ffa774d10f3 --- /dev/null +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_definition_response.yaml @@ -0,0 +1,85 @@ +title: SLO definition response +type: object +required: + - id + - name + - description + - indicator + - timeWindow + - budgetingMethod + - objective + - settings + - revision + - enabled + - groupBy + - tags + - createdAt + - updatedAt + - version +properties: + id: + description: The identifier of the SLO. + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + name: + description: The name of the SLO. + type: string + example: My Service SLO + description: + description: The description of the SLO. + type: string + example: My SLO description + indicator: + discriminator: + propertyName: type + mapping: + sli.apm.transactionErrorRate: './indicator_properties_apm_availability.yaml' + sli.kql.custom: './indicator_properties_custom_kql.yaml' + sli.apm.transactionDuration: './indicator_properties_apm_latency.yaml' + sli.metric.custom: './indicator_properties_custom_metric.yaml' + sli.histogram.custom: './indicator_properties_histogram.yaml' + sli.metric.timeslice: './indicator_properties_timeslice_metric.yaml' + oneOf: + - $ref: "indicator_properties_custom_kql.yaml" + - $ref: "indicator_properties_apm_availability.yaml" + - $ref: "indicator_properties_apm_latency.yaml" + - $ref: "indicator_properties_custom_metric.yaml" + - $ref: "indicator_properties_histogram.yaml" + - $ref: "indicator_properties_timeslice_metric.yaml" + timeWindow: + $ref: "time_window.yaml" + budgetingMethod: + $ref: "budgeting_method.yaml" + objective: + $ref: "objective.yaml" + settings: + $ref: "settings.yaml" + revision: + description: The SLO revision + type: number + example: 2 + enabled: + description: Indicate if the SLO is enabled + type: boolean + example: true + groupBy: + description: optional group by field to use to generate an SLO per distinct value + type: string + example: "some.field" + tags: + description: List of tags + type: array + items: + type: string + createdAt: + description: The creation date + type: string + example: "2023-01-12T10:03:19.000Z" + updatedAt: + description: The last update date + type: string + example: "2023-01-12T10:03:19.000Z" + version: + description: The internal SLO version + type: number + example: 2 \ No newline at end of file diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_with_summary_response.yaml similarity index 96% rename from x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml rename to x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_with_summary_response.yaml index bd58e88c7b641f..df8e35996feb3d 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_with_summary_response.yaml @@ -17,6 +17,7 @@ required: - tags - createdAt - updatedAt + - version properties: id: description: The identifier of the SLO. @@ -86,3 +87,7 @@ properties: description: The last update date type: string example: "2023-01-12T10:03:19.000Z" + version: + description: The internal SLO version + type: number + example: 2 \ No newline at end of file diff --git a/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml b/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml index 910f795aa40a77..10ed40a98d479b 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml @@ -20,11 +20,15 @@ paths: "/s/{spaceId}/api/observability/slos/{sloId}": $ref: "paths/s@{spaceid}@api@slos@{sloid}.yaml" "/s/{spaceId}/api/observability/slos/{sloId}/enable": - $ref: "paths/s@{spaceid}@api@slos@{sloid}@{enable}.yaml" + $ref: "paths/s@{spaceid}@api@slos@{sloid}@enable.yaml" "/s/{spaceId}/api/observability/slos/{sloId}/disable": - $ref: "paths/s@{spaceid}@api@slos@{sloid}@{disable}.yaml" + $ref: "paths/s@{spaceid}@api@slos@{sloid}@disable.yaml" + "/s/{spaceId}/api/observability/slos/{sloId}/_reset": + $ref: "paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml" "/s/{spaceId}/internal/observability/slos/_historical_summary": $ref: "paths/s@{spaceid}@api@slos@_historical_summary.yaml" + "/s/{spaceId}/internal/observability/slos/_definitions": + $ref: "paths/s@{spaceid}@api@slos@_definitions.yaml" "/s/{spaceId}/api/observability/slos/_delete_instances": $ref: "paths/s@{spaceid}@api@slos@_delete_instances.yaml" components: diff --git a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos.yaml b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos.yaml index b606a0aac05fbf..782e8fb477f94a 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos.yaml @@ -68,14 +68,14 @@ get: example: 'slo.name:latency* and slo.tags : "prod"' - name: page in: query - description: The page number to return + description: The page to use for pagination, must be greater or equal than 1 schema: type: integer default: 1 example: 1 - name: perPage in: query - description: The number of SLOs to return per page + description: Number of SLOs returned by page schema: type: integer default: 25 diff --git a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@_definitions.yaml b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@_definitions.yaml new file mode 100644 index 00000000000000..508c3cc86f8fe1 --- /dev/null +++ b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@_definitions.yaml @@ -0,0 +1,62 @@ +get: + summary: Get the SLO definitions + operationId: getDefinitionsOp + description: > + You must have the `read` privileges for the **SLOs** feature in the + **Observability** section of the Kibana feature privileges. + tags: + - slo + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: ../components/parameters/space_id.yaml + - name: includeOutdatedOnly + in: query + description: Indicates if the API returns only outdated SLO or all SLO definitions + schema: + type: boolean + example: true + - name: search + in: query + description: Filters the SLOs by name + schema: + type: string + example: 'my service availability' + - name: page + in: query + description: The page to use for pagination, must be greater or equal than 1 + schema: + type: number + example: 1 + - name: perPage + in: query + description: Number of SLOs returned by page + schema: + type: integer + default: 100 + maximum: 1000 + example: 100 + responses: + '200': + description: Successful request + content: + application/json: + schema: + $ref: '../components/schemas/find_slo_definitions_response.yaml' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '../components/schemas/400_response.yaml' + '401': + description: Unauthorized response + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + '403': + description: Unauthorized response + content: + application/json: + schema: + $ref: '../components/schemas/403_response.yaml' diff --git a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}.yaml b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}.yaml index a7740b7517464c..76d8f0eb640da1 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}.yaml @@ -22,7 +22,7 @@ get: content: application/json: schema: - $ref: '../components/schemas/slo_response.yaml' + $ref: '../components/schemas/slo_with_summary_response.yaml' '400': description: Bad request content: @@ -72,7 +72,7 @@ put: content: application/json: schema: - $ref: '../components/schemas/slo_response.yaml' + $ref: '../components/schemas/slo_definition_response.yaml' '400': description: Bad request content: diff --git a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml new file mode 100644 index 00000000000000..6739d3df78328e --- /dev/null +++ b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml @@ -0,0 +1,43 @@ +post: + summary: Resets an SLO. + operationId: resetSloOp + description: > + You must have the `write` privileges for the **SLOs** feature in the + **Observability** section of the Kibana feature privileges. + tags: + - slo + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: ../components/parameters/space_id.yaml + - $ref: ../components/parameters/slo_id.yaml + responses: + '204': + description: Successful request + content: + application/json: + schema: + $ref: '../components/schemas/slo_definition_response.yaml' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '../components/schemas/400_response.yaml' + '401': + description: Unauthorized response + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + '403': + description: Unauthorized response + content: + application/json: + schema: + $ref: '../components/schemas/403_response.yaml' + '404': + description: Not found response + content: + application/json: + schema: + $ref: '../components/schemas/404_response.yaml' diff --git a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@{disable}.yaml b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@disable.yaml similarity index 100% rename from x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@{disable}.yaml rename to x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@disable.yaml diff --git a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@{enable}.yaml b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@enable.yaml similarity index 100% rename from x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@{enable}.yaml rename to x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@enable.yaml diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx index 5ec21da2efc1c6..6e35ce265bad54 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx @@ -22,7 +22,7 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) { const [options, setOptions] = useState>>([]); const [selectedOptions, setSelectedOptions] = useState>>(); const [searchValue, setSearchValue] = useState(''); - const { isLoading, data: sloList } = useFetchSloDefinitions({ name: searchValue }); + const { isLoading, data } = useFetchSloDefinitions({ name: searchValue }); const hasError = errors !== undefined && errors.length > 0; useEffect(() => { @@ -30,17 +30,17 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) { }, [initialSlo]); useEffect(() => { - const isLoadedWithData = !isLoading && sloList !== undefined; + const isLoadedWithData = !isLoading && !!data?.results; const opts: Array> = isLoadedWithData - ? sloList.map((slo) => ({ value: slo.id, label: slo.name })) + ? data?.results?.map((slo) => ({ value: slo.id, label: slo.name })) : []; setOptions(opts); - }, [isLoading, sloList]); + }, [isLoading, data]); const onChange = (opts: Array>) => { setSelectedOptions(opts); const selectedSlo = - opts.length === 1 ? sloList?.find((slo) => slo.id === opts[0].value) : undefined; + opts.length === 1 ? data?.results?.find((slo) => slo.id === opts[0].value) : undefined; onSelected(selectedSlo); }; diff --git a/x-pack/plugins/observability/public/data/slo/slo.ts b/x-pack/plugins/observability/public/data/slo/slo.ts index 5e210526884fde..c53d55a32839f4 100644 --- a/x-pack/plugins/observability/public/data/slo/slo.ts +++ b/x-pack/plugins/observability/public/data/slo/slo.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { ALL_VALUE, FindSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { cloneDeep } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { ALL_VALUE, FindSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { buildDegradingSummary, buildHealthySummary, @@ -16,8 +16,8 @@ import { buildTimeslicesObjective, buildViolatedSummary, } from './common'; -import { buildCalendarAlignedTimeWindow, buildRollingTimeWindow } from './time_window'; import { buildApmAvailabilityIndicator, buildCustomKqlIndicator } from './indicator'; +import { buildCalendarAlignedTimeWindow, buildRollingTimeWindow } from './time_window'; export const emptySloList: FindSLOResponse = { results: [], @@ -68,6 +68,7 @@ const baseSlo: Omit = { enabled: true, createdAt: now, updatedAt: now, + version: 2, }; export const sloList: FindSLOResponse = { diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts index e74b3570177e4f..b3b7f59dd37cf9 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts @@ -5,24 +5,16 @@ * 2.0. */ -import { FindSloDefinitionsResponse, SLOResponse } from '@kbn/slo-schema'; -import { - QueryObserverResult, - RefetchOptions, - RefetchQueryFilters, - useQuery, -} from '@tanstack/react-query'; +import { FindSLODefinitionsResponse } from '@kbn/slo-schema'; +import { useQuery } from '@tanstack/react-query'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; export interface UseFetchSloDefinitionsResponse { + data: FindSLODefinitionsResponse | undefined; isLoading: boolean; isSuccess: boolean; isError: boolean; - data: SLOResponse[] | undefined; - refetch: ( - options?: (RefetchOptions & RefetchQueryFilters) | undefined - ) => Promise>; } interface Params { @@ -33,18 +25,13 @@ export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefini const { http } = useKibana().services; const search = name.endsWith('*') ? name : `${name}*`; - const { isLoading, isError, isSuccess, data, refetch } = useQuery({ + const { isLoading, isError, isSuccess, data } = useQuery({ queryKey: sloKeys.definitions(search), queryFn: async ({ signal }) => { try { - const response = await http.get( - '/internal/observability/slos/_definitions', - { - query: { - search, - }, - signal, - } + const response = await http.get( + '/api/observability/slos/_definitions', + { query: { search }, signal } ); return response; @@ -56,5 +43,5 @@ export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefini refetchOnWindowFocus: false, }); - return { isLoading, isError, isSuccess, data, refetch }; + return { isLoading, isError, isSuccess, data }; } diff --git a/x-pack/plugins/observability/public/locators/slo_edit.test.ts b/x-pack/plugins/observability/public/locators/slo_edit.test.ts index a01a988dcdb550..cb485ea3e3877d 100644 --- a/x-pack/plugins/observability/public/locators/slo_edit.test.ts +++ b/x-pack/plugins/observability/public/locators/slo_edit.test.ts @@ -20,7 +20,7 @@ describe('SloEditLocator', () => { it('should return correct url when slo is provided', async () => { const location = await locator.getLocation(buildSlo({ id: 'foo' })); expect(location.path).toEqual( - "/slos/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',id:foo,indicator:(params:(filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z')" + "/slos/edit/foo?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',id:foo,indicator:(params:(filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z',version:2)" ); }); }); diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx index de21fb145677f7..c9b04fee85eb25 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx @@ -257,6 +257,7 @@ describe('SLO Details Page', () => { settings, updatedAt, instanceId, + version, ...newSlo } = slo; diff --git a/x-pack/plugins/observability/server/assets/component_templates/slo_mappings_template.ts b/x-pack/plugins/observability/server/assets/component_templates/slo_mappings_template.ts index b4aef3c39a80d4..5e1778596ce167 100644 --- a/x-pack/plugins/observability/server/assets/component_templates/slo_mappings_template.ts +++ b/x-pack/plugins/observability/server/assets/component_templates/slo_mappings_template.ts @@ -12,6 +12,14 @@ export const getSLOMappingsTemplate = (name: string) => ({ template: { mappings: { properties: { + event: { + properties: { + ingested: { + type: 'date', + format: 'strict_date_optional_time', + }, + }, + }, '@timestamp': { type: 'date', format: 'date_optional_time||epoch_millis', @@ -21,11 +29,9 @@ export const getSLOMappingsTemplate = (name: string) => ({ properties: { name: { type: 'keyword', - ignore_above: 256, }, environment: { type: 'keyword', - ignore_above: 256, }, }, }, @@ -33,11 +39,9 @@ export const getSLOMappingsTemplate = (name: string) => ({ properties: { name: { type: 'keyword', - ignore_above: 256, }, type: { type: 'keyword', - ignore_above: 256, }, }, }, @@ -50,56 +54,8 @@ export const getSLOMappingsTemplate = (name: string) => ({ revision: { type: 'long', }, - groupBy: { - type: 'keyword', - ignore_above: 256, - }, instanceId: { type: 'keyword', - ignore_above: 256, - }, - name: { - type: 'keyword', - ignore_above: 256, - }, - description: { - type: 'keyword', - ignore_above: 256, - }, - tags: { - type: 'keyword', - ignore_above: 256, - }, - indicator: { - properties: { - type: { - type: 'keyword', - ignore_above: 256, - }, - }, - }, - objective: { - properties: { - target: { - type: 'double', - }, - sliceDurationInSeconds: { - type: 'long', - }, - }, - }, - budgetingMethod: { - type: 'keyword', - }, - timeWindow: { - properties: { - duration: { - type: 'keyword', - }, - type: { - type: 'keyword', - }, - }, }, numerator: { type: 'long', @@ -110,6 +66,9 @@ export const getSLOMappingsTemplate = (name: string) => ({ isGoodSlice: { type: 'byte', }, + groupings: { + type: 'flattened', + }, }, }, }, diff --git a/x-pack/plugins/observability/server/assets/component_templates/slo_summary_mappings_template.ts b/x-pack/plugins/observability/server/assets/component_templates/slo_summary_mappings_template.ts index 9641b64f5f1d08..c75eb8586334ad 100644 --- a/x-pack/plugins/observability/server/assets/component_templates/slo_summary_mappings_template.ts +++ b/x-pack/plugins/observability/server/assets/component_templates/slo_summary_mappings_template.ts @@ -17,11 +17,9 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({ properties: { name: { type: 'keyword', - ignore_above: 256, }, environment: { type: 'keyword', - ignore_above: 256, }, }, }, @@ -29,11 +27,9 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({ properties: { name: { type: 'keyword', - ignore_above: 256, }, type: { type: 'keyword', - ignore_above: 256, }, }, }, @@ -48,29 +44,49 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({ }, groupBy: { type: 'keyword', - ignore_above: 256, + }, + groupings: { + type: 'flattened', }, instanceId: { type: 'keyword', - ignore_above: 256, + fields: { + text: { + type: 'text', + }, + }, }, name: { - type: 'keyword', - ignore_above: 256, + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, }, description: { - type: 'keyword', - ignore_above: 256, + type: 'text', }, tags: { type: 'keyword', - ignore_above: 256, }, indicator: { properties: { type: { type: 'keyword', - ignore_above: 256, + }, + }, + }, + objective: { + properties: { + target: { + type: 'double', + }, + timesliceTarget: { + type: 'double', + }, + timesliceWindow: { + type: 'keyword', }, }, }, @@ -115,11 +131,21 @@ export const getSLOSummaryMappingsTemplate = (name: string) => ({ }, status: { type: 'keyword', - ignore_above: 32, }, isTempDoc: { type: 'boolean', }, + latestSliTimestamp: { + type: 'date', + format: 'date_optional_time||epoch_millis', + }, + summaryUpdatedAt: { + type: 'date', + format: 'date_optional_time||epoch_millis', + }, + spaceId: { + type: 'keyword', + }, }, }, }, diff --git a/x-pack/plugins/observability/server/assets/ingest_templates/slo_pipeline_template.ts b/x-pack/plugins/observability/server/assets/ingest_templates/slo_pipeline_template.ts index 30724989925d70..6a3a6684ab1918 100644 --- a/x-pack/plugins/observability/server/assets/ingest_templates/slo_pipeline_template.ts +++ b/x-pack/plugins/observability/server/assets/ingest_templates/slo_pipeline_template.ts @@ -9,18 +9,25 @@ import { SLO_RESOURCES_VERSION } from '../../../common/slo/constants'; export const getSLOPipelineTemplate = (id: string, indexNamePrefix: string) => ({ id, - description: 'Monthly date-time index naming for SLO data', + description: 'Ingest pipeline for SLO rollup data', processors: [ + { + set: { + field: 'event.ingested', + value: '{{{_ingest.timestamp}}}', + }, + }, { date_index_name: { field: '@timestamp', index_name_prefix: indexNamePrefix, date_rounding: 'M', + date_formats: ['UNIX_MS', 'ISO8601', "yyyy-MM-dd'T'HH:mm:ss.SSSXX"], }, }, ], _meta: { - description: 'SLO ingest pipeline', + description: 'Ingest pipeline for SLO rollup data', version: SLO_RESOURCES_VERSION, managed: true, managed_by: 'observability', diff --git a/x-pack/plugins/observability/server/assets/ingest_templates/slo_summary_pipeline_template.ts b/x-pack/plugins/observability/server/assets/ingest_templates/slo_summary_pipeline_template.ts index 8246504ac216cd..d279c925f86bf5 100644 --- a/x-pack/plugins/observability/server/assets/ingest_templates/slo_summary_pipeline_template.ts +++ b/x-pack/plugins/observability/server/assets/ingest_templates/slo_summary_pipeline_template.ts @@ -5,56 +5,167 @@ * 2.0. */ -import { SLO_RESOURCES_VERSION } from '../../../common/slo/constants'; +import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; +import { getSLOSummaryPipelineId, SLO_RESOURCES_VERSION } from '../../../common/slo/constants'; +import { SLO } from '../../domain/models'; -export const getSLOSummaryPipelineTemplate = (id: string) => ({ - id, - description: 'SLO summary ingest pipeline', - processors: [ - { - split: { - description: 'Split comma separated list of tags into an array', - field: 'slo.tags', - separator: ',', +export const getSLOSummaryPipelineTemplate = (slo: SLO, spaceId: string) => { + const errorBudgetEstimated = + slo.budgetingMethod === 'occurrences' && slo.timeWindow.type === 'calendarAligned'; + + const optionalObjectiveTimesliceProcessors = timeslicesBudgetingMethodSchema.is( + slo.budgetingMethod + ) + ? [ + { + set: { + description: 'Set objective.timesliceTarget field', + field: 'slo.objective.timesliceTarget', + value: slo.objective.timesliceTarget, + }, + }, + { + set: { + description: 'Set objective.timesliceWindow field', + field: 'slo.objective.timesliceWindow', + value: slo.objective.timesliceWindow!.format(), + }, + }, + ] + : []; + + return { + id: getSLOSummaryPipelineId(slo.id, slo.revision), + description: `Ingest pipeline for SLO summary data [id: ${slo.id}, revision: ${slo.revision}]`, + processors: [ + { + set: { + description: 'Set errorBudgetEstimated field', + field: 'errorBudgetEstimated', + value: errorBudgetEstimated, + }, }, - }, - { - set: { - description: "if 'statusCode == 0', set status to NO_DATA", - if: 'ctx.statusCode == 0', - field: 'status', - value: 'NO_DATA', + { + set: { + description: 'Set isTempDoc field', + field: 'isTempDoc', + value: false, + }, }, - }, - { - set: { - description: "if 'statusCode == 1', set statusLabel to VIOLATED", - if: 'ctx.statusCode == 1', - field: 'status', - value: 'VIOLATED', + { + set: { + description: 'Set groupBy field', + field: 'slo.groupBy', + value: slo.groupBy, + }, }, - }, - { - set: { - description: "if 'statusCode == 2', set status to DEGRADING", - if: 'ctx.statusCode == 2', - field: 'status', - value: 'DEGRADING', + { + set: { + description: 'Set name field', + field: 'slo.name', + value: slo.name, + }, }, - }, - { - set: { - description: "if 'statusCode == 4', set status to HEALTHY", - if: 'ctx.statusCode == 4', - field: 'status', - value: 'HEALTHY', + { + set: { + description: 'Set description field', + field: 'slo.description', + value: slo.description, + }, + }, + { + set: { + description: 'Set tags field', + field: 'slo.tags', + value: slo.tags, + }, + }, + { + set: { + description: 'Set indicator.type field', + field: 'slo.indicator.type', + value: slo.indicator.type, + }, + }, + { + set: { + description: 'Set budgetingMethod field', + field: 'slo.budgetingMethod', + value: slo.budgetingMethod, + }, + }, + { + set: { + description: 'Set timeWindow.duration field', + field: 'slo.timeWindow.duration', + value: slo.timeWindow.duration.format(), + }, + }, + { + set: { + description: 'Set timeWindow.type field', + field: 'slo.timeWindow.type', + value: slo.timeWindow.type, + }, + }, + { + set: { + description: 'Set objective.target field', + field: 'slo.objective.target', + value: slo.objective.target, + }, + }, + ...optionalObjectiveTimesliceProcessors, + { + set: { + description: "if 'statusCode == 0', set status to NO_DATA", + if: 'ctx.statusCode == 0', + field: 'status', + value: 'NO_DATA', + }, + }, + { + set: { + description: "if 'statusCode == 1', set statusLabel to VIOLATED", + if: 'ctx.statusCode == 1', + field: 'status', + value: 'VIOLATED', + }, + }, + { + set: { + description: "if 'statusCode == 2', set status to DEGRADING", + if: 'ctx.statusCode == 2', + field: 'status', + value: 'DEGRADING', + }, + }, + { + set: { + description: "if 'statusCode == 4', set status to HEALTHY", + if: 'ctx.statusCode == 4', + field: 'status', + value: 'HEALTHY', + }, + }, + { + set: { + field: 'summaryUpdatedAt', + value: '{{{_ingest.timestamp}}}', + }, + }, + { + set: { + field: 'spaceId', + value: spaceId, + }, }, + ], + _meta: { + description: `Ingest pipeline for SLO summary data [id: ${slo.id}, revision: ${slo.revision}]`, + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', }, - ], - _meta: { - description: 'SLO summary ingest pipeline', - version: SLO_RESOURCES_VERSION, - managed: true, - managed_by: 'observability', - }, -}); + }; +}; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 6726b8abfe1787..932016b02a4103 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -18,21 +18,21 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/server'; -import { LOG_EXPLORER_LOCATOR_ID, LogExplorerLocatorParams } from '@kbn/deeplinks-observability'; +import { LogExplorerLocatorParams, LOG_EXPLORER_LOCATOR_ID } from '@kbn/deeplinks-observability'; import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server'; import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; import { i18n } from '@kbn/i18n'; -import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; -import { SharePluginSetup } from '@kbn/share-plugin/server'; -import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { ApmRuleType, ES_QUERY_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, } from '@kbn/rule-data-utils'; +import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; +import { SharePluginSetup } from '@kbn/share-plugin/server'; +import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { ObservabilityConfig } from '.'; import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants'; @@ -52,11 +52,7 @@ import { getObservabilityServerRouteRepository } from './routes/get_global_obser import { registerRoutes } from './routes/register_routes'; import { slo, SO_SLO_TYPE } from './saved_objects'; import { threshold } from './saved_objects/threshold'; -import { - DefaultResourceInstaller, - DefaultSLOInstaller, - DefaultSummaryTransformInstaller, -} from './services/slo'; +import { DefaultResourceInstaller, DefaultSLOInstaller } from './services/slo'; import { uiSettings } from './ui_settings'; @@ -75,6 +71,7 @@ interface PluginSetup { interface PluginStart { alerting: PluginStartContract; + spaces?: SpacesPluginStart; } const sloRuleTypes = [SLO_BURN_RATE_RULE_TYPE_ID]; @@ -350,6 +347,7 @@ export class ObservabilityPlugin implements Plugin { ...plugins, core, }, + spaces: pluginStart.spaces, ruleDataService, getRulesClientWithRequest: pluginStart.alerting.getRulesClientWithRequest, }, @@ -360,15 +358,7 @@ export class ObservabilityPlugin implements Plugin { const esInternalClient = coreStart.elasticsearch.client.asInternalUser; const sloResourceInstaller = new DefaultResourceInstaller(esInternalClient, this.logger); - const sloSummaryInstaller = new DefaultSummaryTransformInstaller( - esInternalClient, - this.logger - ); - const sloInstaller = new DefaultSLOInstaller( - sloResourceInstaller, - sloSummaryInstaller, - this.logger - ); + const sloInstaller = new DefaultSLOInstaller(sloResourceInstaller, this.logger); sloInstaller.install(); }); diff --git a/x-pack/plugins/observability/server/routes/register_routes.ts b/x-pack/plugins/observability/server/routes/register_routes.ts index 7726e54793d328..92980f20c4646b 100644 --- a/x-pack/plugins/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability/server/routes/register_routes.ts @@ -14,6 +14,7 @@ import { parseEndpoint, routeValidationObject, } from '@kbn/server-route-repository'; +import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import axios from 'axios'; import * as t from 'io-ts'; import { ObservabilityConfig } from '..'; @@ -33,6 +34,7 @@ export interface RegisterRoutesDependencies { pluginsSetup: { core: CoreSetup; }; + spaces?: SpacesPluginStart; ruleDataService: RuleDataPluginService; getRulesClientWithRequest: (request: KibanaRequest) => RulesClientApi; } diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index ade3f1714ddfba..7ad4b7c36dcc7f 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -19,12 +19,14 @@ import { getSLOInstancesParamsSchema, getSLOParamsSchema, manageSLOParamsSchema, + resetSLOParamsSchema, updateSLOParamsSchema, } from '@kbn/slo-schema'; import type { IndicatorTypes } from '../../domain/models'; import { CreateSLO, DefaultSummaryClient, + DefaultSummaryTransformManager, DefaultTransformManager, DeleteSLO, DeleteSLOInstances, @@ -41,15 +43,17 @@ import { GetPreviewData } from '../../services/slo/get_preview_data'; import { GetSLOInstances } from '../../services/slo/get_slo_instances'; import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client'; import { ManageSLO } from '../../services/slo/manage_slo'; +import { ResetSLO } from '../../services/slo/reset_slo'; import { DefaultSummarySearchClient } from '../../services/slo/summary_search_client'; +import { DefaultSummaryTransformGenerator } from '../../services/slo/summary_transform_generator/summary_transform_generator'; import { ApmTransactionDurationTransformGenerator, ApmTransactionErrorRateTransformGenerator, HistogramTransformGenerator, KQLCustomTransformGenerator, MetricCustomTransformGenerator, - TransformGenerator, TimesliceMetricTransformGenerator, + TransformGenerator, } from '../../services/slo/transform_generators'; import type { ObservabilityRequestHandlerContext } from '../../types'; import { createObservabilityServerRoute } from '../create_observability_server_route'; @@ -79,14 +83,30 @@ const createSLORoute = createObservabilityServerRoute({ access: 'public', }, params: createSLOParamsSchema, - handler: async ({ context, params, logger }) => { + handler: async ({ context, params, logger, dependencies, request }) => { await assertPlatinumLicense(context); + const spaceId = + (await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; const repository = new KibanaSavedObjectsSLORepository(soClient); const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); - const createSLO = new CreateSLO(esClient, repository, transformManager); + const summaryTransformManager = new DefaultSummaryTransformManager( + new DefaultSummaryTransformGenerator(), + esClient, + logger + ); + + const createSLO = new CreateSLO( + esClient, + repository, + transformManager, + summaryTransformManager, + logger, + spaceId + ); const response = await createSLO.execute(params.body); @@ -101,15 +121,30 @@ const updateSLORoute = createObservabilityServerRoute({ access: 'public', }, params: updateSLOParamsSchema, - handler: async ({ context, params, logger }) => { + handler: async ({ context, request, params, logger, dependencies }) => { await assertPlatinumLicense(context); + const spaceId = + (await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; const repository = new KibanaSavedObjectsSLORepository(soClient); const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); - const updateSLO = new UpdateSLO(repository, transformManager, esClient); + const summaryTransformManager = new DefaultSummaryTransformManager( + new DefaultSummaryTransformGenerator(), + esClient, + logger + ); + + const updateSLO = new UpdateSLO( + repository, + transformManager, + summaryTransformManager, + esClient, + logger, + spaceId + ); const response = await updateSLO.execute(params.path.id, params.body); @@ -140,7 +175,19 @@ const deleteSLORoute = createObservabilityServerRoute({ const repository = new KibanaSavedObjectsSLORepository(soClient); const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); - const deleteSLO = new DeleteSLO(repository, transformManager, esClient, rulesClient); + const summaryTransformManager = new DefaultSummaryTransformManager( + new DefaultSummaryTransformGenerator(), + esClient, + logger + ); + + const deleteSLO = new DeleteSLO( + repository, + transformManager, + summaryTransformManager, + esClient, + rulesClient + ); await deleteSLO.execute(params.path.id); }, @@ -183,7 +230,13 @@ const enableSLORoute = createObservabilityServerRoute({ const repository = new KibanaSavedObjectsSLORepository(soClient); const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); - const manageSLO = new ManageSLO(repository, transformManager); + const summaryTransformManager = new DefaultSummaryTransformManager( + new DefaultSummaryTransformGenerator(), + esClient, + logger + ); + + const manageSLO = new ManageSLO(repository, transformManager, summaryTransformManager); const response = await manageSLO.enable(params.path.id); @@ -206,7 +259,13 @@ const disableSLORoute = createObservabilityServerRoute({ const repository = new KibanaSavedObjectsSLORepository(soClient); const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); - const manageSLO = new ManageSLO(repository, transformManager); + const summaryTransformManager = new DefaultSummaryTransformManager( + new DefaultSummaryTransformGenerator(), + esClient, + logger + ); + + const manageSLO = new ManageSLO(repository, transformManager, summaryTransformManager); const response = await manageSLO.disable(params.path.id); @@ -214,6 +273,44 @@ const disableSLORoute = createObservabilityServerRoute({ }, }); +const resetSLORoute = createObservabilityServerRoute({ + endpoint: 'POST /api/observability/slos/{id}/_reset 2023-10-31', + options: { + tags: ['access:slo_write'], + access: 'public', + }, + params: resetSLOParamsSchema, + handler: async ({ context, request, params, logger, dependencies }) => { + await assertPlatinumLicense(context); + + const spaceId = + (await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const soClient = (await context.core).savedObjects.client; + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + + const repository = new KibanaSavedObjectsSLORepository(soClient); + const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); + const summaryTransformManager = new DefaultSummaryTransformManager( + new DefaultSummaryTransformGenerator(), + esClient, + logger + ); + + const resetSLO = new ResetSLO( + esClient, + repository, + transformManager, + summaryTransformManager, + logger, + spaceId + ); + + const response = await resetSLO.execute(params.path.id); + + return response; + }, +}); + const findSLORoute = createObservabilityServerRoute({ endpoint: 'GET /api/observability/slos 2023-10-31', options: { @@ -221,13 +318,16 @@ const findSLORoute = createObservabilityServerRoute({ access: 'public', }, params: findSLOParamsSchema, - handler: async ({ context, params, logger }) => { + handler: async ({ context, request, params, logger, dependencies }) => { await assertPlatinumLicense(context); + const spaceId = + (await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const repository = new KibanaSavedObjectsSLORepository(soClient); - const summarySearchClient = new DefaultSummarySearchClient(esClient, logger); + const summarySearchClient = new DefaultSummarySearchClient(esClient, logger, spaceId); const findSLO = new FindSLO(repository, summarySearchClient); const response = await findSLO.execute(params?.query ?? {}); @@ -253,10 +353,9 @@ const deleteSloInstancesRoute = createObservabilityServerRoute({ }); const findSloDefinitionsRoute = createObservabilityServerRoute({ - endpoint: 'GET /internal/observability/slos/_definitions', + endpoint: 'GET /api/observability/slos/_definitions 2023-10-31', options: { tags: ['access:slo_read'], - access: 'internal', }, params: findSloDefinitionsParamsSchema, handler: async ({ context, params }) => { @@ -266,7 +365,7 @@ const findSloDefinitionsRoute = createObservabilityServerRoute({ const repository = new KibanaSavedObjectsSLORepository(soClient); const findSloDefinitions = new FindSLODefinitions(repository); - const response = await findSloDefinitions.execute(params.query.search); + const response = await findSloDefinitions.execute(params?.query ?? {}); return response; }, @@ -395,4 +494,5 @@ export const sloRouteRepository = { ...getSloBurnRates, ...getPreviewData, ...getSLOInstancesRoute, + ...resetSLORoute, }; diff --git a/x-pack/plugins/observability/server/saved_objects/slo.ts b/x-pack/plugins/observability/server/saved_objects/slo.ts index 41cb509d837556..058596e160fd71 100644 --- a/x-pack/plugins/observability/server/saved_objects/slo.ts +++ b/x-pack/plugins/observability/server/saved_objects/slo.ts @@ -17,7 +17,6 @@ type StoredSLOBefore890 = StoredSLO & { isCalendar?: boolean; }; }; - const migrateSlo890: SavedObjectMigrationFn = (doc) => { const { timeWindow, ...other } = doc.attributes; return { @@ -38,6 +37,21 @@ export const slo: SavedObjectsType = { name: SO_SLO_TYPE, hidden: false, namespaceType: 'multiple-isolated', + switchToModelVersionAt: '8.10.0', + modelVersions: { + 1: { + changes: [ + { type: 'mappings_addition', addedMappings: { version: { type: 'long' } } }, + { + type: 'data_backfill', + backfillFn: (doc) => { + // we explicitely set the version to 1, so we know which SLOs requires a migration to the following version. + return { attributes: { version: doc.attributes.version ?? 1 } }; + }, + }, + ], + }, + }, mappings: { dynamic: false, properties: { @@ -53,6 +67,7 @@ export const slo: SavedObjectsType = { budgetingMethod: { type: 'keyword' }, enabled: { type: 'boolean' }, tags: { type: 'keyword' }, + version: { type: 'long' }, }, }, management: { diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap index e66f1f8124a11f..8c2cfc3b0d1f5f 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap @@ -1,6 +1,144 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CreateSLO happy path calls the expected services 1`] = ` +Array [ + Object { + "_meta": Object { + "description": "Ingest pipeline for SLO summary data [id: unique-id, revision: 1]", + "managed": true, + "managed_by": "observability", + "version": 3, + }, + "description": "Ingest pipeline for SLO summary data [id: unique-id, revision: 1]", + "id": ".slo-observability.summary.pipeline-unique-id-1", + "processors": Array [ + Object { + "set": Object { + "description": "Set errorBudgetEstimated field", + "field": "errorBudgetEstimated", + "value": false, + }, + }, + Object { + "set": Object { + "description": "Set isTempDoc field", + "field": "isTempDoc", + "value": false, + }, + }, + Object { + "set": Object { + "description": "Set groupBy field", + "field": "slo.groupBy", + "value": "*", + }, + }, + Object { + "set": Object { + "description": "Set name field", + "field": "slo.name", + "value": "irrelevant", + }, + }, + Object { + "set": Object { + "description": "Set description field", + "field": "slo.description", + "value": "irrelevant", + }, + }, + Object { + "set": Object { + "description": "Set tags field", + "field": "slo.tags", + "value": Array [], + }, + }, + Object { + "set": Object { + "description": "Set indicator.type field", + "field": "slo.indicator.type", + "value": "sli.apm.transactionErrorRate", + }, + }, + Object { + "set": Object { + "description": "Set budgetingMethod field", + "field": "slo.budgetingMethod", + "value": "occurrences", + }, + }, + Object { + "set": Object { + "description": "Set timeWindow.duration field", + "field": "slo.timeWindow.duration", + "value": "7d", + }, + }, + Object { + "set": Object { + "description": "Set timeWindow.type field", + "field": "slo.timeWindow.type", + "value": "rolling", + }, + }, + Object { + "set": Object { + "description": "Set objective.target field", + "field": "slo.objective.target", + "value": 0.99, + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 0', set status to NO_DATA", + "field": "status", + "if": "ctx.statusCode == 0", + "value": "NO_DATA", + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 1', set statusLabel to VIOLATED", + "field": "status", + "if": "ctx.statusCode == 1", + "value": "VIOLATED", + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 2', set status to DEGRADING", + "field": "status", + "if": "ctx.statusCode == 2", + "value": "DEGRADING", + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 4', set status to HEALTHY", + "field": "status", + "if": "ctx.statusCode == 4", + "value": "HEALTHY", + }, + }, + Object { + "set": Object { + "field": "summaryUpdatedAt", + "value": "{{{_ingest.timestamp}}}", + }, + }, + Object { + "set": Object { + "field": "spaceId", + "value": "some-space", + }, + }, + ], + }, +] +`; + +exports[`CreateSLO happy path calls the expected services 2`] = ` Array [ Object { "document": Object { @@ -25,6 +163,11 @@ Array [ }, "instanceId": "*", "name": "irrelevant", + "objective": Object { + "target": 0.99, + "timesliceTarget": null, + "timesliceWindow": null, + }, "revision": 1, "tags": Array [], "timeWindow": Object { @@ -32,6 +175,7 @@ Array [ "type": "rolling", }, }, + "spaceId": "some-space", "status": "NO_DATA", "statusCode": 0, "totalEvents": 0, @@ -41,7 +185,7 @@ Array [ }, }, "id": "slo-unique-id", - "index": ".slo-observability.summary-v2.temp", + "index": ".slo-observability.summary-v3.temp", "refresh": true, }, ] diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/delete_slo.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/delete_slo.test.ts.snap new file mode 100644 index 00000000000000..3edfb0cd194b02 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/delete_slo.test.ts.snap @@ -0,0 +1,177 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DeleteSLO happy path removes all resources associatde to the slo 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "irrelevant", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 2`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 3`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 4`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 5`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 6`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "id": ".slo-observability.summary.pipeline-irrelevant-1", + }, + Object { + "ignore": Array [ + 404, + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 7`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "index": ".slo-observability.sli-v3*", + "query": Object { + "match": Object { + "slo.id": "irrelevant", + }, + }, + "wait_for_completion": false, + }, + ], + Array [ + Object { + "index": ".slo-observability.summary-v3*", + "query": Object { + "match": Object { + "slo.id": "irrelevant", + }, + }, + "refresh": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 8`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "filter": "alert.attributes.params.sloId:irrelevant", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`DeleteSLO happy path removes all resources associatde to the slo 9`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "irrelevant", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/get_slo_instances.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/get_slo_instances.test.ts.snap index be3b681db0af8f..8ad9792a22b243 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/get_slo_instances.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/get_slo_instances.test.ts.snap @@ -11,7 +11,7 @@ Array [ }, }, }, - "index": ".slo-observability.sli-v2*", + "index": ".slo-observability.sli-v3*", "query": Object { "bool": Object { "filter": Array [ diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/manage_slo.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/manage_slo.test.ts.snap new file mode 100644 index 00000000000000..aff53e48828185 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/manage_slo.test.ts.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ManageSLO Disable disables the slo when enabled 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ManageSLO Disable disables the slo when enabled 2`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ManageSLO Enable enables the slo when disabled 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ManageSLO Enable enables the slo when disabled 2`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/reset_slo.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/reset_slo.test.ts.snap new file mode 100644 index 00000000000000..9ad1d09bd1ef84 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/reset_slo.test.ts.snap @@ -0,0 +1,489 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResetSLO resets all associated resources 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 2`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 3`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 4`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 5`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "index": ".slo-observability.sli-v3*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "slo.id": "irrelevant", + }, + }, + ], + }, + }, + "refresh": true, + }, + ], + Array [ + Object { + "index": ".slo-observability.summary-v3*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "slo.id": "irrelevant", + }, + }, + ], + }, + }, + "refresh": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 6`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "budgetingMethod": "occurrences", + "createdAt": 2023-01-01T00:00:00.000Z, + "description": "irrelevant", + "enabled": true, + "groupBy": "*", + "id": "irrelevant", + "indicator": Object { + "params": Object { + "environment": "irrelevant", + "index": "metrics-apm*", + "service": "irrelevant", + "threshold": 500, + "transactionName": "irrelevant", + "transactionType": "irrelevant", + }, + "type": "sli.apm.transactionDuration", + }, + "name": "irrelevant", + "objective": Object { + "target": 0.999, + }, + "revision": 1, + "settings": Object { + "frequency": Duration { + "unit": "m", + "value": 1, + }, + "syncDelay": Duration { + "unit": "m", + "value": 1, + }, + }, + "tags": Array [ + "critical", + "k8s", + ], + "timeWindow": Object { + "duration": Duration { + "unit": "d", + "value": 7, + }, + "type": "rolling", + }, + "updatedAt": 2023-01-01T00:00:00.000Z, + "version": 1, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 7`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 8`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "_meta": Object { + "description": "Ingest pipeline for SLO summary data [id: irrelevant, revision: 1]", + "managed": true, + "managed_by": "observability", + "version": 3, + }, + "description": "Ingest pipeline for SLO summary data [id: irrelevant, revision: 1]", + "id": ".slo-observability.summary.pipeline-irrelevant-1", + "processors": Array [ + Object { + "set": Object { + "description": "Set errorBudgetEstimated field", + "field": "errorBudgetEstimated", + "value": false, + }, + }, + Object { + "set": Object { + "description": "Set isTempDoc field", + "field": "isTempDoc", + "value": false, + }, + }, + Object { + "set": Object { + "description": "Set groupBy field", + "field": "slo.groupBy", + "value": "*", + }, + }, + Object { + "set": Object { + "description": "Set name field", + "field": "slo.name", + "value": "irrelevant", + }, + }, + Object { + "set": Object { + "description": "Set description field", + "field": "slo.description", + "value": "irrelevant", + }, + }, + Object { + "set": Object { + "description": "Set tags field", + "field": "slo.tags", + "value": Array [ + "critical", + "k8s", + ], + }, + }, + Object { + "set": Object { + "description": "Set indicator.type field", + "field": "slo.indicator.type", + "value": "sli.apm.transactionDuration", + }, + }, + Object { + "set": Object { + "description": "Set budgetingMethod field", + "field": "slo.budgetingMethod", + "value": "occurrences", + }, + }, + Object { + "set": Object { + "description": "Set timeWindow.duration field", + "field": "slo.timeWindow.duration", + "value": "7d", + }, + }, + Object { + "set": Object { + "description": "Set timeWindow.type field", + "field": "slo.timeWindow.type", + "value": "rolling", + }, + }, + Object { + "set": Object { + "description": "Set objective.target field", + "field": "slo.objective.target", + "value": 0.999, + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 0', set status to NO_DATA", + "field": "status", + "if": "ctx.statusCode == 0", + "value": "NO_DATA", + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 1', set statusLabel to VIOLATED", + "field": "status", + "if": "ctx.statusCode == 1", + "value": "VIOLATED", + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 2', set status to DEGRADING", + "field": "status", + "if": "ctx.statusCode == 2", + "value": "DEGRADING", + }, + }, + Object { + "set": Object { + "description": "if 'statusCode == 4', set status to HEALTHY", + "field": "status", + "if": "ctx.statusCode == 4", + "value": "HEALTHY", + }, + }, + Object { + "set": Object { + "field": "summaryUpdatedAt", + "value": "{{{_ingest.timestamp}}}", + }, + }, + Object { + "set": Object { + "field": "spaceId", + "value": "some-space", + }, + }, + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 9`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "budgetingMethod": "occurrences", + "createdAt": 2023-01-01T00:00:00.000Z, + "description": "irrelevant", + "enabled": true, + "groupBy": "*", + "id": "irrelevant", + "indicator": Object { + "params": Object { + "environment": "irrelevant", + "index": "metrics-apm*", + "service": "irrelevant", + "threshold": 500, + "transactionName": "irrelevant", + "transactionType": "irrelevant", + }, + "type": "sli.apm.transactionDuration", + }, + "name": "irrelevant", + "objective": Object { + "target": 0.999, + }, + "revision": 1, + "settings": Object { + "frequency": Duration { + "unit": "m", + "value": 1, + }, + "syncDelay": Duration { + "unit": "m", + "value": 1, + }, + }, + "tags": Array [ + "critical", + "k8s", + ], + "timeWindow": Object { + "duration": Duration { + "unit": "d", + "value": 7, + }, + "type": "rolling", + }, + "updatedAt": 2023-01-01T00:00:00.000Z, + "version": 1, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 10`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-irrelevant-1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`ResetSLO resets all associated resources 11`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "document": Object { + "errorBudgetConsumed": 0, + "errorBudgetEstimated": false, + "errorBudgetInitial": 0.0010000000000000009, + "errorBudgetRemaining": 1, + "goodEvents": 0, + "isTempDoc": true, + "service": Object { + "environment": null, + "name": null, + }, + "sliValue": -1, + "slo": Object { + "budgetingMethod": "occurrences", + "description": "irrelevant", + "groupBy": "*", + "id": "irrelevant", + "indicator": Object { + "type": "sli.apm.transactionDuration", + }, + "instanceId": "*", + "name": "irrelevant", + "objective": Object { + "target": 0.999, + "timesliceTarget": null, + "timesliceWindow": null, + }, + "revision": 1, + "tags": Array [ + "critical", + "k8s", + ], + "timeWindow": Object { + "duration": "7d", + "type": "rolling", + }, + }, + "spaceId": "some-space", + "status": "NO_DATA", + "statusCode": 0, + "totalEvents": 0, + "transaction": Object { + "name": null, + "type": null, + }, + }, + "id": "slo-irrelevant", + "index": ".slo-observability.summary-v3.temp", + "refresh": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`; diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap index ea94e840aac0e0..daf5e47a0a66cb 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap @@ -3,7 +3,7 @@ exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 1`] = ` Array [ Object { - "index": ".slo-observability.summary-v2*", + "index": ".slo-observability.summary-v3*", "query": Object { "bool": Object { "filter": Array [ diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap index ae7a966951f7c3..c1f0099d243430 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap @@ -1,51 +1,88 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UpdateSLO index a temporary summary document 1`] = ` -Array [ - Object { - "document": Object { - "errorBudgetConsumed": 0, - "errorBudgetEstimated": false, - "errorBudgetInitial": 0.0010000000000000009, - "errorBudgetRemaining": 1, - "goodEvents": 0, - "isTempDoc": true, - "service": Object { - "environment": null, - "name": null, +exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-original-id-2", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 2`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-summary-original-id-2", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 3`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-original-id-2", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 4`] = ` +[MockFunction] { + "calls": Array [ + Array [ + "slo-original-id-2", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`UpdateSLO when error happens during the update restores the previous SLO definition in the repository 5`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "id": ".slo-observability.summary.pipeline-original-id-2", }, - "sliValue": -1, - "slo": Object { - "budgetingMethod": "occurrences", - "description": "irrelevant", - "groupBy": "*", - "id": "unique-id", - "indicator": Object { - "type": "sli.apm.transactionErrorRate", - }, - "instanceId": "*", - "name": "irrelevant", - "revision": 2, - "tags": Array [ - "critical", - "k8s", + Object { + "ignore": Array [ + 404, ], - "timeWindow": Object { - "duration": "7d", - "type": "rolling", - }, - }, - "status": "NO_DATA", - "statusCode": 0, - "totalEvents": 0, - "transaction": Object { - "name": null, - "type": null, }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, }, - "id": "slo-unique-id", - "index": ".slo-observability.summary-v2.temp", - "refresh": true, - }, -] + ], +} `; 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 bd34d652e5fa4b..fe8de00589db62 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 @@ -5,25 +5,45 @@ * 2.0. */ -import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { + ElasticsearchClientMock, + elasticsearchServiceMock, + loggingSystemMock, +} from '@kbn/core/server/mocks'; +import { MockedLogger } from '@kbn/logging-mocks'; import { CreateSLO } from './create_slo'; import { fiveMinute, oneMinute } from './fixtures/duration'; import { createAPMTransactionErrorRateIndicator, createSLOParams } from './fixtures/slo'; -import { createSLORepositoryMock, createTransformManagerMock } from './mocks'; +import { + createSLORepositoryMock, + createSummaryTransformManagerMock, + createTransformManagerMock, +} from './mocks'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; describe('CreateSLO', () => { let esClientMock: ElasticsearchClientMock; + let loggerMock: jest.Mocked; let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; + let mockSummaryTransformManager: jest.Mocked; let createSLO: CreateSLO; beforeEach(() => { esClientMock = elasticsearchServiceMock.createElasticsearchClient(); + loggerMock = loggingSystemMock.createLogger(); mockRepository = createSLORepositoryMock(); mockTransformManager = createTransformManagerMock(); - createSLO = new CreateSLO(esClientMock, mockRepository, mockTransformManager); + mockSummaryTransformManager = createSummaryTransformManagerMock(); + createSLO = new CreateSLO( + esClientMock, + mockRepository, + mockTransformManager, + mockSummaryTransformManager, + loggerMock, + 'some-space' + ); }); describe('happy path', () => { @@ -32,7 +52,8 @@ describe('CreateSLO', () => { id: 'unique-id', indicator: createAPMTransactionErrorRateIndicator(), }); - mockTransformManager.install.mockResolvedValue('slo-transform-id'); + mockTransformManager.install.mockResolvedValue('slo-id-revision'); + mockSummaryTransformManager.install.mockResolvedValue('slo-summary-id-revision'); const response = await createSLO.execute(sloParams); @@ -47,18 +68,21 @@ describe('CreateSLO', () => { revision: 1, tags: [], enabled: true, + version: 2, createdAt: expect.any(Date), updatedAt: expect.any(Date), }), { throwOnConflict: true } ); - 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(mockTransformManager.install).toHaveBeenCalled(); + expect(mockTransformManager.start).toHaveBeenCalled(); + expect(esClientMock.ingest.putPipeline.mock.calls[0]).toMatchSnapshot(); + expect(mockSummaryTransformManager.install).toHaveBeenCalled(); + expect(mockSummaryTransformManager.start).toHaveBeenCalled(); expect(esClientMock.index.mock.calls[0]).toMatchSnapshot(); + + expect(response).toEqual(expect.objectContaining({ id: 'unique-id' })); }); it('overrides the default values when provided', async () => { @@ -93,32 +117,20 @@ describe('CreateSLO', () => { }); describe('unhappy path', () => { - it('deletes the SLO when transform installation fails', async () => { - mockTransformManager.install.mockRejectedValue(new Error('Transform install error')); - const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); - - await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform install error'); - 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')); + it('rollbacks new resources on failure', async () => { + mockTransformManager.install.mockRejectedValue(new Error('Rollup transform install 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')); - const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); + await expect(createSLO.execute(sloParams)).rejects.toThrowError( + 'Rollup transform install error' + ); - await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform start error'); - expect(mockTransformManager.uninstall).toBeCalledWith('slo-transform-id'); - expect(mockRepository.deleteById).toBeCalled(); + expect(mockSummaryTransformManager.stop).toHaveBeenCalled(); + expect(mockSummaryTransformManager.uninstall).toHaveBeenCalled(); + expect(mockTransformManager.stop).toHaveBeenCalled(); + expect(mockTransformManager.uninstall).toHaveBeenCalled(); + expect(esClientMock.ingest.deletePipeline).toHaveBeenCalled(); + expect(mockRepository.deleteById).toHaveBeenCalled(); }); }); }); 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 cebe8188a7cbdc..d7e116d983584c 100644 --- a/x-pack/plugins/observability/server/services/slo/create_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/create_slo.ts @@ -5,21 +5,32 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ALL_VALUE, CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema'; import { v4 as uuidv4 } from 'uuid'; -import { SLO_SUMMARY_TEMP_INDEX_NAME } from '../../../common/slo/constants'; +import { + getSLOSummaryPipelineId, + getSLOSummaryTransformId, + getSLOTransformId, + SLO_MODEL_VERSION, + SLO_SUMMARY_TEMP_INDEX_NAME, +} from '../../../common/slo/constants'; +import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template'; import { Duration, DurationUnit, SLO } from '../../domain/models'; import { validateSLO } from '../../domain/services'; +import { retryTransientEsErrors } from '../../utils/retry'; import { SLORepository } from './slo_repository'; -import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary'; +import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; export class CreateSLO { constructor( private esClient: ElasticsearchClient, private repository: SLORepository, - private transformManager: TransformManager + private transformManager: TransformManager, + private summaryTransformManager: TransformManager, + private logger: Logger, + private spaceId: string ) {} public async execute(params: CreateSLOParams): Promise { @@ -27,33 +38,48 @@ export class CreateSLO { validateSLO(slo); await this.repository.save(slo, { throwOnConflict: true }); - let sloTransformId; - try { - sloTransformId = await this.transformManager.install(slo); - } catch (err) { - await this.repository.deleteById(slo.id); - throw err; - } + const rollupTransformId = getSLOTransformId(slo.id, slo.revision); + const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); try { - await this.transformManager.preview(sloTransformId); - await this.transformManager.start(sloTransformId); + await this.transformManager.install(slo); + await this.transformManager.start(rollupTransformId); + await retryTransientEsErrors( + () => this.esClient.ingest.putPipeline(getSLOSummaryPipelineTemplate(slo, this.spaceId)), + { logger: this.logger } + ); + + await this.summaryTransformManager.install(slo); + await this.summaryTransformManager.start(summaryTransformId); + + await retryTransientEsErrors( + () => + this.esClient.index({ + index: SLO_SUMMARY_TEMP_INDEX_NAME, + id: `slo-${slo.id}`, + document: createTempSummaryDocument(slo, this.spaceId), + refresh: true, + }), + { logger: this.logger } + ); } catch (err) { - await Promise.all([ - this.transformManager.uninstall(sloTransformId), - this.repository.deleteById(slo.id), - ]); + this.logger.error( + `Cannot install the SLO [id: ${slo.id}, revision: ${slo.revision}]. Rolling back.` + ); + + await this.summaryTransformManager.stop(summaryTransformId); + await this.summaryTransformManager.uninstall(summaryTransformId); + await this.transformManager.stop(rollupTransformId); + await this.transformManager.uninstall(rollupTransformId); + await this.esClient.ingest.deletePipeline( + { id: getSLOSummaryPipelineId(slo.id, slo.revision) }, + { ignore: [404] } + ); + await this.repository.deleteById(slo.id); throw err; } - await this.esClient.index({ - index: SLO_SUMMARY_TEMP_INDEX_NAME, - id: `slo-${slo.id}`, - document: createTempSummaryDocument(slo), - refresh: true, - }); - return this.toResponse(slo); } @@ -72,6 +98,7 @@ export class CreateSLO { createdAt: now, updatedAt: now, groupBy: !!params.groupBy ? params.groupBy : ALL_VALUE, + version: SLO_MODEL_VERSION, }; } diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts index 8a19890e2ebc2c..506151da864d37 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts @@ -9,20 +9,20 @@ import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock'; import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; import { ElasticsearchClient } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; -import { - getSLOTransformId, - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_PATTERN, -} from '../../../common/slo/constants'; import { DeleteSLO } from './delete_slo'; import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo'; -import { createSLORepositoryMock, createTransformManagerMock } from './mocks'; +import { + createSLORepositoryMock, + createSummaryTransformManagerMock, + createTransformManagerMock, +} from './mocks'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; describe('DeleteSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; + let mockSummaryTransformManager: jest.Mocked; let mockEsClient: jest.Mocked; let mockRulesClient: jest.Mocked; let deleteSLO: DeleteSLO; @@ -30,52 +30,37 @@ describe('DeleteSLO', () => { beforeEach(() => { mockRepository = createSLORepositoryMock(); mockTransformManager = createTransformManagerMock(); + mockSummaryTransformManager = createSummaryTransformManagerMock(); mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); mockRulesClient = rulesClientMock.create(); - deleteSLO = new DeleteSLO(mockRepository, mockTransformManager, mockEsClient, mockRulesClient); + deleteSLO = new DeleteSLO( + mockRepository, + mockTransformManager, + mockSummaryTransformManager, + mockEsClient, + mockRulesClient + ); }); describe('happy path', () => { - it('removes the transform, the roll up data, the associated rules and the SLO from the repository', async () => { - const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() }); + it('removes all resources associatde to the slo', async () => { + const slo = createSLO({ + id: 'irrelevant', + indicator: createAPMTransactionErrorRateIndicator(), + }); mockRepository.findById.mockResolvedValueOnce(slo); await deleteSLO.execute(slo.id); - expect(mockRepository.findById).toHaveBeenCalledWith(slo.id); - expect(mockTransformManager.stop).toHaveBeenCalledWith( - getSLOTransformId(slo.id, slo.revision) - ); - expect(mockTransformManager.uninstall).toHaveBeenCalledWith( - getSLOTransformId(slo.id, slo.revision) - ); - expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2); - expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - index: SLO_DESTINATION_INDEX_PATTERN, - query: { - match: { - 'slo.id': slo.id, - }, - }, - }) - ); - expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - query: { - match: { - 'slo.id': slo.id, - }, - }, - }) - ); - expect(mockRulesClient.bulkDeleteRules).toHaveBeenCalledWith({ - filter: `alert.attributes.params.sloId:${slo.id}`, - }); - expect(mockRepository.deleteById).toHaveBeenCalledWith(slo.id); + expect(mockRepository.findById).toMatchSnapshot(); + expect(mockSummaryTransformManager.stop).toMatchSnapshot(); + expect(mockSummaryTransformManager.uninstall).toMatchSnapshot(); + expect(mockTransformManager.stop).toMatchSnapshot(); + expect(mockTransformManager.uninstall).toMatchSnapshot(); + expect(mockEsClient.ingest.deletePipeline).toMatchSnapshot(); + expect(mockEsClient.deleteByQuery).toMatchSnapshot(); + expect(mockRulesClient.bulkDeleteRules).toMatchSnapshot(); + expect(mockRepository.deleteById).toMatchSnapshot(); }); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.ts index 78c3bffd05417c..e3d66638602224 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.ts @@ -8,10 +8,13 @@ import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; import { ElasticsearchClient } from '@kbn/core/server'; import { + getSLOSummaryPipelineId, + getSLOSummaryTransformId, getSLOTransformId, SLO_DESTINATION_INDEX_PATTERN, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, } from '../../../common/slo/constants'; +import { retryTransientEsErrors } from '../../utils/retry'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; @@ -19,6 +22,7 @@ export class DeleteSLO { constructor( private repository: SLORepository, private transformManager: TransformManager, + private summaryTransformManager: TransformManager, private esClient: ElasticsearchClient, private rulesClient: RulesClientApi ) {} @@ -26,9 +30,20 @@ export class DeleteSLO { public async execute(sloId: string): Promise { const slo = await this.repository.findById(sloId); - const sloTransformId = getSLOTransformId(slo.id, slo.revision); - await this.transformManager.stop(sloTransformId); - await this.transformManager.uninstall(sloTransformId); + const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); + await this.summaryTransformManager.stop(summaryTransformId); + await this.summaryTransformManager.uninstall(summaryTransformId); + + const rollupTransformId = getSLOTransformId(slo.id, slo.revision); + await this.transformManager.stop(rollupTransformId); + await this.transformManager.uninstall(rollupTransformId); + + await retryTransientEsErrors(() => + this.esClient.ingest.deletePipeline( + { id: getSLOSummaryPipelineId(slo.id, slo.revision) }, + { ignore: [404] } + ) + ); await this.deleteRollupData(slo.id); await this.deleteSummaryData(slo.id); diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo_instances.test.ts b/x-pack/plugins/observability/server/services/slo/delete_slo_instances.test.ts index 8a9c64a6b441cf..ca4eac790bd04f 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo_instances.test.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo_instances.test.ts @@ -43,7 +43,7 @@ describe('DeleteSLOInstances', () => { expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2); expect(mockEsClient.deleteByQuery.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "index": ".slo-observability.sli-v2*", + "index": ".slo-observability.sli-v3*", "query": Object { "bool": Object { "should": Array [ @@ -103,7 +103,7 @@ describe('DeleteSLOInstances', () => { `); expect(mockEsClient.deleteByQuery.mock.calls[1][0]).toMatchInlineSnapshot(` Object { - "index": ".slo-observability.summary-v2*", + "index": ".slo-observability.summary-v3*", "query": Object { "bool": Object { "should": Array [ diff --git a/x-pack/plugins/observability/server/services/slo/find_slo.test.ts b/x-pack/plugins/observability/server/services/slo/find_slo.test.ts index 10436bc0fad541..e8d80ef8e74f5b 100644 --- a/x-pack/plugins/observability/server/services/slo/find_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/find_slo.test.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { ALL_VALUE } from '@kbn/slo-schema'; +import { ALL_VALUE, Paginated } from '@kbn/slo-schema'; +import { SLO_MODEL_VERSION } from '../../../common/slo/constants'; import { SLO } from '../../domain/models'; import { FindSLO } from './find_slo'; import { createSLO } from './fixtures/slo'; import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks'; import { SLORepository } from './slo_repository'; -import { Paginated, SLOSummary, SummarySearchClient } from './summary_search_client'; +import { SLOSummary, SummarySearchClient } from './summary_search_client'; describe('FindSLO', () => { let mockRepository: jest.Mocked; @@ -95,6 +96,7 @@ describe('FindSLO', () => { revision: slo.revision, groupBy: slo.groupBy, instanceId: ALL_VALUE, + version: SLO_MODEL_VERSION, }, ], }); @@ -147,7 +149,7 @@ describe('FindSLO', () => { await expect(findSLO.execute({ perPage: '5000' })).resolves.not.toThrow(); await expect(findSLO.execute({ perPage: '5001' })).rejects.toThrowError( - 'perPage limit to 5000' + 'perPage limit set to 5000' ); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/find_slo.ts b/x-pack/plugins/observability/server/services/slo/find_slo.ts index cf8150db3e627a..fb90ec86d04d5c 100644 --- a/x-pack/plugins/observability/server/services/slo/find_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/find_slo.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { FindSLOParams, FindSLOResponse, findSLOResponseSchema } from '@kbn/slo-schema'; +import { FindSLOParams, FindSLOResponse, findSLOResponseSchema, Pagination } from '@kbn/slo-schema'; import { SLO, SLOWithSummary } from '../../domain/models'; import { IllegalArgumentError } from '../../errors'; import { SLORepository } from './slo_repository'; -import { Pagination, SLOSummary, Sort, SummarySearchClient } from './summary_search_client'; +import { SLOSummary, Sort, SummarySearchClient } from './summary_search_client'; const DEFAULT_PAGE = 1; const DEFAULT_PER_PAGE = 25; @@ -55,7 +55,7 @@ function toPagination(params: FindSLOParams): Pagination { const perPage = Number(params.perPage); if (!isNaN(perPage) && perPage > MAX_PER_PAGE) { - throw new IllegalArgumentError('perPage limit to 5000'); + throw new IllegalArgumentError(`perPage limit set to ${MAX_PER_PAGE}`); } return { diff --git a/x-pack/plugins/observability/server/services/slo/find_slo_definitions.ts b/x-pack/plugins/observability/server/services/slo/find_slo_definitions.ts index 157e5b4be5696d..38076d67202d5b 100644 --- a/x-pack/plugins/observability/server/services/slo/find_slo_definitions.ts +++ b/x-pack/plugins/observability/server/services/slo/find_slo_definitions.ts @@ -5,14 +5,40 @@ * 2.0. */ -import { FindSloDefinitionsResponse, findSloDefinitionsResponseSchema } from '@kbn/slo-schema'; +import { + FindSLODefinitionsParams, + FindSLODefinitionsResponse, + findSloDefinitionsResponseSchema, + Pagination, +} from '@kbn/slo-schema'; +import { IllegalArgumentError } from '../../errors'; import { SLORepository } from './slo_repository'; +const MAX_PER_PAGE = 1000; +const DEFAULT_PER_PAGE = 100; +const DEFAULT_PAGE = 1; + export class FindSLODefinitions { constructor(private repository: SLORepository) {} - public async execute(search: string): Promise { - const sloList = await this.repository.search(search); - return findSloDefinitionsResponseSchema.encode(sloList); + public async execute(params: FindSLODefinitionsParams): Promise { + const result = await this.repository.search(params.search ?? '', toPagination(params), { + includeOutdatedOnly: params.includeOutdatedOnly === true ? true : false, + }); + return findSloDefinitionsResponseSchema.encode(result); + } +} + +function toPagination(params: FindSLODefinitionsParams): Pagination { + const page = Number(params.page); + const perPage = Number(params.perPage); + + if (!isNaN(perPage) && perPage > MAX_PER_PAGE) { + throw new IllegalArgumentError(`perPage limit set to ${MAX_PER_PAGE}`); } + + return { + page: !isNaN(page) && page >= 1 ? page : DEFAULT_PAGE, + perPage: !isNaN(perPage) && perPage >= 1 ? perPage : DEFAULT_PER_PAGE, + }; } diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts index 2bd320cbb8d659..0f75c837754893 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts @@ -15,6 +15,7 @@ import { } from '@kbn/slo-schema'; import { cloneDeep } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; +import { SLO_MODEL_VERSION } from '../../../../common/slo/constants'; import { APMTransactionDurationIndicator, APMTransactionErrorRateIndicator, @@ -139,7 +140,7 @@ export const createHistogramIndicator = ( }, }); -const defaultSLO: Omit = { +const defaultSLO: Omit = { name: 'irrelevant', description: 'irrelevant', timeWindow: sevenDaysRolling(), @@ -190,6 +191,7 @@ export const createSLO = (params: Partial = {}): SLO => { revision: 1, createdAt: now, updatedAt: now, + version: SLO_MODEL_VERSION, ...params, }); }; diff --git a/x-pack/plugins/observability/server/services/slo/get_slo.test.ts b/x-pack/plugins/observability/server/services/slo/get_slo.test.ts index 1a5efccc9eb2af..18fe85cdfb0b1d 100644 --- a/x-pack/plugins/observability/server/services/slo/get_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/get_slo.test.ts @@ -6,6 +6,7 @@ */ import { ALL_VALUE } from '@kbn/slo-schema'; +import { SLO_MODEL_VERSION } from '../../../common/slo/constants'; import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo'; import { GetSLO } from './get_slo'; import { createSummaryClientMock, createSLORepositoryMock } from './mocks'; @@ -84,6 +85,7 @@ describe('GetSLO', () => { revision: slo.revision, groupBy: slo.groupBy, instanceId: ALL_VALUE, + version: SLO_MODEL_VERSION, }); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/index.ts b/x-pack/plugins/observability/server/services/slo/index.ts index 7c99c289ae90bf..2a939c56fde4b5 100644 --- a/x-pack/plugins/observability/server/services/slo/index.ts +++ b/x-pack/plugins/observability/server/services/slo/index.ts @@ -14,10 +14,10 @@ export * from './get_slo'; export * from './historical_summary_client'; export * from './resource_installer'; export * from './slo_installer'; -export * from './summary_transform/summary_transform_installer'; export * from './sli_client'; export * from './slo_repository'; export * from './transform_manager'; +export * from './summay_transform_manager'; export * from './update_slo'; export * from './summary_client'; export * from './get_slo_instances'; diff --git a/x-pack/plugins/observability/server/services/slo/manage_slo.test.ts b/x-pack/plugins/observability/server/services/slo/manage_slo.test.ts index 78396fa74cbee7..bace47b69ff4bc 100644 --- a/x-pack/plugins/observability/server/services/slo/manage_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/manage_slo.test.ts @@ -7,19 +7,26 @@ import { createSLO } from './fixtures/slo'; import { ManageSLO } from './manage_slo'; -import { createSLORepositoryMock, createTransformManagerMock } from './mocks'; +import { + createSLORepositoryMock, + createSummaryTransformManagerMock, + createTransformManagerMock, +} from './mocks'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; describe('ManageSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; + let mockSummaryTransformManager: jest.Mocked; let manageSLO: ManageSLO; beforeEach(() => { mockRepository = createSLORepositoryMock(); mockTransformManager = createTransformManagerMock(); - manageSLO = new ManageSLO(mockRepository, mockTransformManager); + mockSummaryTransformManager = createSummaryTransformManagerMock(); + + manageSLO = new ManageSLO(mockRepository, mockTransformManager, mockSummaryTransformManager); }); describe('Enable', () => { @@ -30,16 +37,18 @@ describe('ManageSLO', () => { await manageSLO.enable(slo.id); expect(mockTransformManager.start).not.toHaveBeenCalled(); + expect(mockSummaryTransformManager.start).not.toHaveBeenCalled(); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('enables the slo when disabled', async () => { - const slo = createSLO({ enabled: false }); + const slo = createSLO({ id: 'irrelevant', enabled: false }); mockRepository.findById.mockResolvedValue(slo); await manageSLO.enable(slo.id); - expect(mockTransformManager.start).toHaveBeenCalled(); + expect(mockTransformManager.start).toMatchSnapshot(); + expect(mockSummaryTransformManager.start).toMatchSnapshot(); expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({ enabled: true })); }); }); @@ -52,16 +61,18 @@ describe('ManageSLO', () => { await manageSLO.disable(slo.id); expect(mockTransformManager.stop).not.toHaveBeenCalled(); + expect(mockSummaryTransformManager.stop).not.toHaveBeenCalled(); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('disables the slo when enabled', async () => { - const slo = createSLO({ enabled: true }); + const slo = createSLO({ id: 'irrelevant', enabled: true }); mockRepository.findById.mockResolvedValue(slo); await manageSLO.disable(slo.id); - expect(mockTransformManager.stop).toHaveBeenCalled(); + expect(mockTransformManager.stop).toMatchSnapshot(); + expect(mockSummaryTransformManager.stop).toMatchSnapshot(); expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({ enabled: false })); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/manage_slo.ts b/x-pack/plugins/observability/server/services/slo/manage_slo.ts index b7220f084a64d9..9d9fd6eb1705c1 100644 --- a/x-pack/plugins/observability/server/services/slo/manage_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/manage_slo.ts @@ -5,12 +5,16 @@ * 2.0. */ -import { getSLOTransformId } from '../../../common/slo/constants'; +import { getSLOSummaryTransformId, getSLOTransformId } from '../../../common/slo/constants'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; export class ManageSLO { - constructor(private repository: SLORepository, private transformManager: TransformManager) {} + constructor( + private repository: SLORepository, + private transformManager: TransformManager, + private summaryTransformManager: TransformManager + ) {} async enable(sloId: string) { const slo = await this.repository.findById(sloId); @@ -18,6 +22,7 @@ export class ManageSLO { return; } + await this.summaryTransformManager.start(getSLOSummaryTransformId(slo.id, slo.revision)); await this.transformManager.start(getSLOTransformId(slo.id, slo.revision)); slo.enabled = true; slo.updatedAt = new Date(); @@ -30,6 +35,7 @@ export class ManageSLO { return; } + await this.summaryTransformManager.stop(getSLOSummaryTransformId(slo.id, slo.revision)); await this.transformManager.stop(getSLOTransformId(slo.id, slo.revision)); slo.enabled = false; slo.updatedAt = new Date(); 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 979c3057d1e8e8..eb8db093a71740 100644 --- a/x-pack/plugins/observability/server/services/slo/mocks/index.ts +++ b/x-pack/plugins/observability/server/services/slo/mocks/index.ts @@ -10,7 +10,6 @@ import { SLIClient } from '../sli_client'; import { SLORepository } from '../slo_repository'; import { SummaryClient } from '../summary_client'; import { SummarySearchClient } from '../summary_search_client'; -import { SummaryTransformInstaller } from '../summary_transform/summary_transform_installer'; import { TransformManager } from '../transform_manager'; const createResourceInstallerMock = (): jest.Mocked => { @@ -19,13 +18,17 @@ const createResourceInstallerMock = (): jest.Mocked => { }; }; -const createSummaryTransformInstallerMock = (): jest.Mocked => { +const createTransformManagerMock = (): jest.Mocked => { return { - installAndStart: jest.fn(), + install: jest.fn(), + preview: jest.fn(), + uninstall: jest.fn(), + start: jest.fn(), + stop: jest.fn(), }; }; -const createTransformManagerMock = (): jest.Mocked => { +const createSummaryTransformManagerMock = (): jest.Mocked => { return { install: jest.fn(), preview: jest.fn(), @@ -65,8 +68,8 @@ const createSLIClientMock = (): jest.Mocked => { export { createResourceInstallerMock, - createSummaryTransformInstallerMock, createTransformManagerMock, + createSummaryTransformManagerMock, createSLORepositoryMock, createSummaryClientMock, createSummarySearchClientMock, diff --git a/x-pack/plugins/observability/server/services/slo/reset_slo.test.ts b/x-pack/plugins/observability/server/services/slo/reset_slo.test.ts new file mode 100644 index 00000000000000..feae6695fbd33e --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/reset_slo.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { MockedLogger } from '@kbn/logging-mocks'; + +import { SLO_MODEL_VERSION } from '../../../common/slo/constants'; +import { createSLO } from './fixtures/slo'; +import { + createSLORepositoryMock, + createSummaryTransformManagerMock, + createTransformManagerMock, +} from './mocks'; +import { ResetSLO } from './reset_slo'; +import { SLORepository } from './slo_repository'; +import { TransformManager } from './transform_manager'; + +const TEST_DATE = new Date('2023-01-01T00:00:00.000Z'); + +describe('ResetSLO', () => { + let mockRepository: jest.Mocked; + let mockTransformManager: jest.Mocked; + let mockSummaryTransformManager: jest.Mocked; + let mockEsClient: jest.Mocked; + let loggerMock: jest.Mocked; + let resetSLO: ResetSLO; + + beforeEach(() => { + loggerMock = loggingSystemMock.createLogger(); + mockRepository = createSLORepositoryMock(); + mockTransformManager = createTransformManagerMock(); + mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + mockSummaryTransformManager = createSummaryTransformManagerMock(); + resetSLO = new ResetSLO( + mockEsClient, + mockRepository, + mockTransformManager, + mockSummaryTransformManager, + loggerMock, + 'some-space' + ); + jest.useFakeTimers().setSystemTime(TEST_DATE); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('resets all associated resources', async () => { + const slo = createSLO({ id: 'irrelevant', version: 1 }); + mockRepository.findById.mockResolvedValueOnce(slo); + mockRepository.save.mockImplementation((v) => Promise.resolve(v)); + + await resetSLO.execute(slo.id); + + // delete existing resources and data + expect(mockSummaryTransformManager.stop).toMatchSnapshot(); + expect(mockSummaryTransformManager.uninstall).toMatchSnapshot(); + + expect(mockTransformManager.stop).toMatchSnapshot(); + expect(mockTransformManager.uninstall).toMatchSnapshot(); + + expect(mockEsClient.deleteByQuery).toMatchSnapshot(); + + // install resources + expect(mockSummaryTransformManager.install).toMatchSnapshot(); + expect(mockSummaryTransformManager.start).toMatchSnapshot(); + + expect(mockEsClient.ingest.putPipeline).toMatchSnapshot(); + + expect(mockTransformManager.install).toMatchSnapshot(); + expect(mockTransformManager.start).toMatchSnapshot(); + + expect(mockEsClient.index).toMatchSnapshot(); + + expect(mockRepository.save).toHaveBeenCalledWith({ + ...slo, + version: SLO_MODEL_VERSION, + updatedAt: expect.anything(), + }); + }); +}); diff --git a/x-pack/plugins/observability/server/services/slo/reset_slo.ts b/x-pack/plugins/observability/server/services/slo/reset_slo.ts new file mode 100644 index 00000000000000..8c4a3747619796 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/reset_slo.ts @@ -0,0 +1,130 @@ +/* + * 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 { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { resetSLOResponseSchema } from '@kbn/slo-schema'; +import { + getSLOSummaryPipelineId, + getSLOSummaryTransformId, + getSLOTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_MODEL_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_TEMP_INDEX_NAME, +} from '../../../common/slo/constants'; +import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template'; +import { retryTransientEsErrors } from '../../utils/retry'; +import { SLORepository } from './slo_repository'; +import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; +import { TransformManager } from './transform_manager'; + +export class ResetSLO { + constructor( + private esClient: ElasticsearchClient, + private repository: SLORepository, + private transformManager: TransformManager, + private summaryTransformManager: TransformManager, + private logger: Logger, + private spaceId: string + ) {} + + public async execute(sloId: string) { + const slo = await this.repository.findById(sloId); + + const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); + await this.summaryTransformManager.stop(summaryTransformId); + await this.summaryTransformManager.uninstall(summaryTransformId); + + const rollupTransformId = getSLOTransformId(slo.id, slo.revision); + await this.transformManager.stop(rollupTransformId); + await this.transformManager.uninstall(rollupTransformId); + + await Promise.all([this.deleteRollupData(slo.id), this.deleteSummaryData(slo.id)]); + + try { + await this.transformManager.install(slo); + await this.transformManager.start(rollupTransformId); + await retryTransientEsErrors( + () => this.esClient.ingest.putPipeline(getSLOSummaryPipelineTemplate(slo, this.spaceId)), + { logger: this.logger } + ); + + await this.summaryTransformManager.install(slo); + await this.summaryTransformManager.start(summaryTransformId); + + await retryTransientEsErrors( + () => + this.esClient.index({ + index: SLO_SUMMARY_TEMP_INDEX_NAME, + id: `slo-${slo.id}`, + document: createTempSummaryDocument(slo, this.spaceId), + refresh: true, + }), + { logger: this.logger } + ); + } catch (err) { + this.logger.error( + `Cannot reset the SLO [id: ${slo.id}, revision: ${slo.revision}]. Rolling back.` + ); + + await this.summaryTransformManager.stop(summaryTransformId); + await this.summaryTransformManager.uninstall(summaryTransformId); + await this.transformManager.stop(rollupTransformId); + await this.transformManager.uninstall(rollupTransformId); + await this.esClient.ingest.deletePipeline( + { id: getSLOSummaryPipelineId(slo.id, slo.revision) }, + { ignore: [404] } + ); + + throw err; + } + + const updatedSlo = await this.repository.save({ + ...slo, + version: SLO_MODEL_VERSION, + updatedAt: new Date(), + }); + + return resetSLOResponseSchema.encode(updatedSlo); + } + + /** + * Deleting all SLI rollup data matching the sloId. All revision will be deleted in case of + * residual documents. + * + * @param sloId + */ + private async deleteRollupData(sloId: string): Promise { + await this.esClient.deleteByQuery({ + index: SLO_DESTINATION_INDEX_PATTERN, + refresh: true, + query: { + bool: { + filter: [{ term: { 'slo.id': sloId } }], + }, + }, + }); + } + + /** + * Deleting the summary documents matching the sloId. All revision will be deleted in case of + * residual documents. + * + * @param sloId + */ + private async deleteSummaryData(sloId: string): Promise { + await this.esClient.deleteByQuery({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + refresh: true, + query: { + bool: { + filter: [{ term: { 'slo.id': sloId } }], + }, + }, + }); + } +} diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts index 6634b3bc052af0..6fe98ebf8f6b21 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts @@ -15,7 +15,6 @@ import { SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_SUMMARY_INDEX_TEMPLATE_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, } from '../../../common/slo/constants'; import { DefaultResourceInstaller } from './resource_installer'; @@ -54,14 +53,10 @@ describe('resourceInstaller', () => { expect.objectContaining({ name: SLO_SUMMARY_INDEX_TEMPLATE_NAME }) ); - expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(2); + expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(1); expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith( 1, expect.objectContaining({ id: SLO_INGEST_PIPELINE_NAME }) ); - expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME }) - ); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.ts index 2975e07333599b..c639f4cc6c3d77 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.ts @@ -28,13 +28,11 @@ import { SLO_SUMMARY_DESTINATION_INDEX_NAME, SLO_SUMMARY_INDEX_TEMPLATE_NAME, SLO_SUMMARY_INDEX_TEMPLATE_PATTERN, - SLO_SUMMARY_INGEST_PIPELINE_NAME, SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../../common/slo/constants'; import { getSLOIndexTemplate } from '../../assets/index_templates/slo_index_templates'; import { getSLOSummaryIndexTemplate } from '../../assets/index_templates/slo_summary_index_templates'; import { getSLOPipelineTemplate } from '../../assets/ingest_templates/slo_pipeline_template'; -import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template'; import { retryTransientEsErrors } from '../../utils/retry'; export interface ResourceInstaller { @@ -87,10 +85,6 @@ export class DefaultResourceInstaller implements ResourceInstaller { await this.createOrUpdateIngestPipelineTemplate( getSLOPipelineTemplate(SLO_INGEST_PIPELINE_NAME, SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX) ); - - await this.createOrUpdateIngestPipelineTemplate( - getSLOSummaryPipelineTemplate(SLO_SUMMARY_INGEST_PIPELINE_NAME) - ); } catch (err) { this.logger.error(`Error installing resources shared for SLO: ${err.message}`); throw err; diff --git a/x-pack/plugins/observability/server/services/slo/slo_installer.test.ts b/x-pack/plugins/observability/server/services/slo/slo_installer.test.ts index 6bd9f798c62340..92d0865ec5c9f5 100644 --- a/x-pack/plugins/observability/server/services/slo/slo_installer.test.ts +++ b/x-pack/plugins/observability/server/services/slo/slo_installer.test.ts @@ -7,7 +7,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { MockedLogger } from '@kbn/logging-mocks'; -import { createResourceInstallerMock, createSummaryTransformInstallerMock } from './mocks'; +import { createResourceInstallerMock } from './mocks'; import { DefaultSLOInstaller } from './slo_installer'; describe('SLO Installer', () => { @@ -19,16 +19,10 @@ describe('SLO Installer', () => { it.skip('handles concurrent installation', async () => { const resourceInstaller = createResourceInstallerMock(); - const summaryTransformInstaller = createSummaryTransformInstallerMock(); - const service = new DefaultSLOInstaller( - resourceInstaller, - summaryTransformInstaller, - loggerMock - ); + const service = new DefaultSLOInstaller(resourceInstaller, loggerMock); await Promise.all([service.install(), service.install()]); expect(resourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalledTimes(1); - expect(summaryTransformInstaller.installAndStart).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/slo_installer.ts b/x-pack/plugins/observability/server/services/slo/slo_installer.ts index bdee31f62912dc..9484ecd907d9f3 100644 --- a/x-pack/plugins/observability/server/services/slo/slo_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/slo_installer.ts @@ -6,7 +6,7 @@ */ import { Logger } from '@kbn/core/server'; -import { ResourceInstaller, SummaryTransformInstaller } from '.'; +import { ResourceInstaller } from '.'; export interface SLOInstaller { install(): Promise; @@ -15,11 +15,7 @@ export interface SLOInstaller { export class DefaultSLOInstaller implements SLOInstaller { private isInstalling: boolean = false; - constructor( - private sloResourceInstaller: ResourceInstaller, - private sloSummaryInstaller: SummaryTransformInstaller, - private logger: Logger - ) {} + constructor(private sloResourceInstaller: ResourceInstaller, private logger: Logger) {} public async install() { if (this.isInstalling) { @@ -32,9 +28,8 @@ export class DefaultSLOInstaller implements SLOInstaller { installTimeout = setTimeout(() => (this.isInstalling = false), 60000); await this.sloResourceInstaller.ensureCommonResourcesInstalled(); - await this.sloSummaryInstaller.installAndStart(); } catch (error) { - this.logger.error('Failed to install SLO common resources and summary transforms'); + this.logger.error('Failed to install SLO common resources'); } finally { this.isInstalling = false; clearTimeout(installTimeout); diff --git a/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts b/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts index 4b61bff04ea6f6..65248d487a392b 100644 --- a/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts +++ b/x-pack/plugins/observability/server/services/slo/slo_repository.test.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { sloSchema } from '@kbn/slo-schema'; +import { SLO_MODEL_VERSION } from '../../../common/slo/constants'; import { SLO, StoredSLO } from '../../domain/models'; import { SLOIdConflict, SLONotFound } from '../../errors'; import { SO_SLO_TYPE } from '../../saved_objects'; @@ -164,19 +165,42 @@ describe('KibanaSavedObjectsSLORepository', () => { expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, SOME_SLO.id); }); - it('searches by name', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO])); + describe('search', () => { + it('searches by name', async () => { + const repository = new KibanaSavedObjectsSLORepository(soClientMock); + soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO])); - const results = await repository.search(SOME_SLO.name); + const results = await repository.search(SOME_SLO.name, { page: 1, perPage: 100 }); - expect(results).toEqual([SOME_SLO, ANOTHER_SLO]); - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - search: SOME_SLO.name, - searchFields: ['name'], + expect(results.results).toEqual([SOME_SLO, ANOTHER_SLO]); + expect(soClientMock.find).toHaveBeenCalledWith({ + type: SO_SLO_TYPE, + page: 1, + perPage: 100, + search: SOME_SLO.name, + searchFields: ['name'], + }); + }); + + it('searches only the outdated ones', async () => { + const repository = new KibanaSavedObjectsSLORepository(soClientMock); + soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO])); + + const results = await repository.search( + SOME_SLO.name, + { page: 1, perPage: 100 }, + { includeOutdatedOnly: true } + ); + + expect(results.results).toEqual([SOME_SLO, ANOTHER_SLO]); + expect(soClientMock.find).toHaveBeenCalledWith({ + type: SO_SLO_TYPE, + page: 1, + perPage: 100, + search: SOME_SLO.name, + searchFields: ['name'], + filter: `slo.attributes.version < ${SLO_MODEL_VERSION}`, + }); }); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/slo_repository.ts b/x-pack/plugins/observability/server/services/slo/slo_repository.ts index cc595ed0b00993..be6c9266b9e90d 100644 --- a/x-pack/plugins/observability/server/services/slo/slo_repository.ts +++ b/x-pack/plugins/observability/server/services/slo/slo_repository.ts @@ -7,10 +7,11 @@ import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; -import { sloSchema } from '@kbn/slo-schema'; +import { Paginated, Pagination, sloSchema } from '@kbn/slo-schema'; import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { SLO_MODEL_VERSION } from '../../../common/slo/constants'; import { SLO, StoredSLO } from '../../domain/models'; import { SLOIdConflict, SLONotFound } from '../../errors'; import { SO_SLO_TYPE } from '../../saved_objects'; @@ -20,7 +21,11 @@ export interface SLORepository { findAllByIds(ids: string[]): Promise; findById(id: string): Promise; deleteById(id: string): Promise; - search(search: string): Promise; + search( + search: string, + pagination: Pagination, + options?: { includeOutdatedOnly?: boolean } + ): Promise>; } export class KibanaSavedObjectsSLORepository implements SLORepository { @@ -99,19 +104,28 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { } } - async search(search: string): Promise { - try { - const response = await this.soClient.find({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - search, - searchFields: ['name'], - }); - return response.saved_objects.map((slo) => toSLO(slo.attributes)); - } catch (err) { - throw err; - } + async search( + search: string, + pagination: Pagination, + options: { includeOutdatedOnly?: boolean } = { includeOutdatedOnly: false } + ): Promise> { + const response = await this.soClient.find({ + type: SO_SLO_TYPE, + page: pagination.page, + perPage: pagination.perPage, + search, + searchFields: ['name'], + ...(!!options.includeOutdatedOnly && { + filter: `slo.attributes.version < ${SLO_MODEL_VERSION}`, + }), + }); + + return { + total: response.total, + perPage: response.per_page, + page: response.page, + results: response.saved_objects.map((slo) => toSLO(slo.attributes)), + }; } } @@ -121,7 +135,13 @@ function toStoredSLO(slo: SLO): StoredSLO { function toSLO(storedSLO: StoredSLO): SLO { return pipe( - sloSchema.decode(storedSLO), + sloSchema.decode({ + ...storedSLO, + // version was added in 8.12.0. This is a safeguard against SO migration issue. + // if not present, we considered the version to be 1, e.g. not migrated. + // We would need to call the _reset api on this SLO. + version: storedSLO.version ?? 1, + }), fold(() => { throw new Error('Invalid Stored SLO'); }, t.identity) diff --git a/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts b/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts index 5e6f50398fc95a..256aa9164ea3bc 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts @@ -7,17 +7,13 @@ import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; +import { Pagination } from '@kbn/slo-schema/src/models/pagination'; import { aHitFromSummaryIndex, aHitFromTempSummaryIndex, aSummaryDocument, } from './fixtures/summary_search_document'; -import { - DefaultSummarySearchClient, - Pagination, - Sort, - SummarySearchClient, -} from './summary_search_client'; +import { DefaultSummarySearchClient, Sort, SummarySearchClient } from './summary_search_client'; const defaultSort: Sort = { field: 'sli_value', @@ -34,7 +30,7 @@ describe('Summary Search Client', () => { beforeEach(() => { esClientMock = elasticsearchServiceMock.createElasticsearchClient(); - service = new DefaultSummarySearchClient(esClientMock, loggerMock.create()); + service = new DefaultSummarySearchClient(esClientMock, loggerMock.create(), 'some-space'); }); it('returns an empty response on error', async () => { diff --git a/x-pack/plugins/observability/server/services/slo/summary_search_client.ts b/x-pack/plugins/observability/server/services/slo/summary_search_client.ts index a145a76aa729d0..9715d727f6fc57 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_search_client.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_search_client.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { ALL_VALUE } from '@kbn/slo-schema'; +import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; import _ from 'lodash'; import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; @@ -31,13 +31,6 @@ interface EsSummaryDocument { isTempDoc: boolean; } -export interface Paginated { - total: number; - page: number; - perPage: number; - results: T[]; -} - export interface SLOSummary { id: SLOId; instanceId: string; @@ -50,17 +43,16 @@ export interface Sort { direction: 'asc' | 'desc'; } -export interface Pagination { - page: number; - perPage: number; -} - export interface SummarySearchClient { search(kqlQuery: string, sort: Sort, pagination: Pagination): Promise>; } export class DefaultSummarySearchClient implements SummarySearchClient { - constructor(private esClient: ElasticsearchClient, private logger: Logger) {} + constructor( + private esClient: ElasticsearchClient, + private logger: Logger, + private spaceId: string + ) {} async search( kqlQuery: string, @@ -71,7 +63,11 @@ export class DefaultSummarySearchClient implements SummarySearchClient { const summarySearch = await this.esClient.search({ index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, track_total_hits: true, - query: getElastichsearchQueryOrThrow(kqlQuery), + query: { + bool: { + filter: [{ term: { spaceId: this.spaceId } }, getElastichsearchQueryOrThrow(kqlQuery)], + }, + }, sort: { // non-temp first, then temp documents isTempDoc: { 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 deleted file mode 100644 index 5161c5e5cb51b8..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/__snapshots__/summary_transform_installer.test.ts.snap +++ /dev/null @@ -1,1192 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Summary Transform Installer installs only the missing summary transforms 1`] = ` -Array [ - Array [ - Object { - "_meta": Object { - "managed": true, - "managed_by": "observability", - "version": 3, - }, - "description": "Summarize every SLO with timeslices budgeting method and a 7 days rolling time window", - "dest": Object { - "index": ".slo-observability.summary-v2", - "pipeline": ".slo-observability.summary.pipeline", - }, - "frequency": "1m", - "pivot": Object { - "aggregations": Object { - "_objectiveTarget": Object { - "max": Object { - "field": "slo.objective.target", - }, - }, - "errorBudgetConsumed": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetInitial": "errorBudgetInitial", - "sliValue": "sliValue", - }, - "script": "if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }", - }, - }, - "errorBudgetInitial": Object { - "bucket_script": Object { - "buckets_path": Object { - "objectiveTarget": "_objectiveTarget", - }, - "script": "1 - params.objectiveTarget", - }, - }, - "errorBudgetRemaining": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetConsummed": "errorBudgetConsumed", - }, - "script": "1 - params.errorBudgetConsummed", - }, - }, - "goodEvents": Object { - "sum": Object { - "field": "slo.isGoodSlice", - }, - }, - "sliValue": Object { - "bucket_script": Object { - "buckets_path": Object { - "goodEvents": "goodEvents", - "totalEvents": "totalEvents", - }, - "script": "if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }", - }, - }, - "statusCode": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetRemaining": "errorBudgetRemaining", - "objectiveTarget": "_objectiveTarget", - "sliValue": "sliValue", - }, - "script": Object { - "source": "if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }", - }, - }, - }, - "totalEvents": Object { - "value_count": Object { - "field": "slo.isGoodSlice", - }, - }, - }, - "group_by": Object { - "errorBudgetEstimated": Object { - "terms": Object { - "field": "errorBudgetEstimated", - }, - }, - "isTempDoc": Object { - "terms": Object { - "field": "isTempDoc", - }, - }, - "service.environment": Object { - "terms": Object { - "field": "service.environment", - "missing_bucket": true, - }, - }, - "service.name": Object { - "terms": Object { - "field": "service.name", - "missing_bucket": true, - }, - }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, - "slo.id": Object { - "terms": Object { - "field": "slo.id", - }, - }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, - "slo.instanceId": Object { - "terms": Object { - "field": "slo.instanceId", - }, - }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.revision": Object { - "terms": Object { - "field": "slo.revision", - }, - }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, - "transaction.name": Object { - "terms": Object { - "field": "transaction.name", - "missing_bucket": true, - }, - }, - "transaction.type": Object { - "terms": Object { - "field": "transaction.type", - "missing_bucket": true, - }, - }, - }, - }, - "settings": Object { - "deduce_mappings": false, - "unattended": true, - }, - "source": Object { - "index": ".slo-observability.sli-v2*", - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "gte": "now-7d/m", - "lte": "now/m", - }, - }, - }, - Object { - "term": Object { - "slo.budgetingMethod": "timeslices", - }, - }, - Object { - "term": Object { - "slo.timeWindow.type": "rolling", - }, - }, - Object { - "term": Object { - "slo.timeWindow.duration": "7d", - }, - }, - ], - }, - }, - "runtime_mappings": Object { - "errorBudgetEstimated": Object { - "script": "emit(false)", - "type": "boolean", - }, - "isTempDoc": Object { - "script": "emit(false)", - "type": "boolean", - }, - }, - }, - "sync": Object { - "time": Object { - "delay": "125s", - "field": "@timestamp", - }, - }, - "transform_id": "slo-summary-timeslices-7d-rolling", - }, - Object { - "ignore": Array [ - 409, - ], - }, - ], - Array [ - Object { - "_meta": Object { - "managed": true, - "managed_by": "observability", - "version": 3, - }, - "description": "Summarize every SLO with timeslices budgeting method and a 30 days rolling time window", - "dest": Object { - "index": ".slo-observability.summary-v2", - "pipeline": ".slo-observability.summary.pipeline", - }, - "frequency": "1m", - "pivot": Object { - "aggregations": Object { - "_objectiveTarget": Object { - "max": Object { - "field": "slo.objective.target", - }, - }, - "errorBudgetConsumed": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetInitial": "errorBudgetInitial", - "sliValue": "sliValue", - }, - "script": "if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }", - }, - }, - "errorBudgetInitial": Object { - "bucket_script": Object { - "buckets_path": Object { - "objectiveTarget": "_objectiveTarget", - }, - "script": "1 - params.objectiveTarget", - }, - }, - "errorBudgetRemaining": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetConsummed": "errorBudgetConsumed", - }, - "script": "1 - params.errorBudgetConsummed", - }, - }, - "goodEvents": Object { - "sum": Object { - "field": "slo.isGoodSlice", - }, - }, - "sliValue": Object { - "bucket_script": Object { - "buckets_path": Object { - "goodEvents": "goodEvents", - "totalEvents": "totalEvents", - }, - "script": "if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }", - }, - }, - "statusCode": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetRemaining": "errorBudgetRemaining", - "objectiveTarget": "_objectiveTarget", - "sliValue": "sliValue", - }, - "script": Object { - "source": "if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }", - }, - }, - }, - "totalEvents": Object { - "value_count": Object { - "field": "slo.isGoodSlice", - }, - }, - }, - "group_by": Object { - "errorBudgetEstimated": Object { - "terms": Object { - "field": "errorBudgetEstimated", - }, - }, - "isTempDoc": Object { - "terms": Object { - "field": "isTempDoc", - }, - }, - "service.environment": Object { - "terms": Object { - "field": "service.environment", - "missing_bucket": true, - }, - }, - "service.name": Object { - "terms": Object { - "field": "service.name", - "missing_bucket": true, - }, - }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, - "slo.id": Object { - "terms": Object { - "field": "slo.id", - }, - }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, - "slo.instanceId": Object { - "terms": Object { - "field": "slo.instanceId", - }, - }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.revision": Object { - "terms": Object { - "field": "slo.revision", - }, - }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, - "transaction.name": Object { - "terms": Object { - "field": "transaction.name", - "missing_bucket": true, - }, - }, - "transaction.type": Object { - "terms": Object { - "field": "transaction.type", - "missing_bucket": true, - }, - }, - }, - }, - "settings": Object { - "deduce_mappings": false, - "unattended": true, - }, - "source": Object { - "index": ".slo-observability.sli-v2*", - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "gte": "now-30d/m", - "lte": "now/m", - }, - }, - }, - Object { - "term": Object { - "slo.budgetingMethod": "timeslices", - }, - }, - Object { - "term": Object { - "slo.timeWindow.type": "rolling", - }, - }, - Object { - "term": Object { - "slo.timeWindow.duration": "30d", - }, - }, - ], - }, - }, - "runtime_mappings": Object { - "errorBudgetEstimated": Object { - "script": "emit(false)", - "type": "boolean", - }, - "isTempDoc": Object { - "script": "emit(false)", - "type": "boolean", - }, - }, - }, - "sync": Object { - "time": Object { - "delay": "125s", - "field": "@timestamp", - }, - }, - "transform_id": "slo-summary-timeslices-30d-rolling", - }, - Object { - "ignore": Array [ - 409, - ], - }, - ], - Array [ - Object { - "_meta": Object { - "managed": true, - "managed_by": "observability", - "version": 3, - }, - "description": "Summarize every SLO with timeslices budgeting method and a 90 days rolling time window", - "dest": Object { - "index": ".slo-observability.summary-v2", - "pipeline": ".slo-observability.summary.pipeline", - }, - "frequency": "1m", - "pivot": Object { - "aggregations": Object { - "_objectiveTarget": Object { - "max": Object { - "field": "slo.objective.target", - }, - }, - "errorBudgetConsumed": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetInitial": "errorBudgetInitial", - "sliValue": "sliValue", - }, - "script": "if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }", - }, - }, - "errorBudgetInitial": Object { - "bucket_script": Object { - "buckets_path": Object { - "objectiveTarget": "_objectiveTarget", - }, - "script": "1 - params.objectiveTarget", - }, - }, - "errorBudgetRemaining": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetConsummed": "errorBudgetConsumed", - }, - "script": "1 - params.errorBudgetConsummed", - }, - }, - "goodEvents": Object { - "sum": Object { - "field": "slo.isGoodSlice", - }, - }, - "sliValue": Object { - "bucket_script": Object { - "buckets_path": Object { - "goodEvents": "goodEvents", - "totalEvents": "totalEvents", - }, - "script": "if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }", - }, - }, - "statusCode": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetRemaining": "errorBudgetRemaining", - "objectiveTarget": "_objectiveTarget", - "sliValue": "sliValue", - }, - "script": Object { - "source": "if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }", - }, - }, - }, - "totalEvents": Object { - "value_count": Object { - "field": "slo.isGoodSlice", - }, - }, - }, - "group_by": Object { - "errorBudgetEstimated": Object { - "terms": Object { - "field": "errorBudgetEstimated", - }, - }, - "isTempDoc": Object { - "terms": Object { - "field": "isTempDoc", - }, - }, - "service.environment": Object { - "terms": Object { - "field": "service.environment", - "missing_bucket": true, - }, - }, - "service.name": Object { - "terms": Object { - "field": "service.name", - "missing_bucket": true, - }, - }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, - "slo.id": Object { - "terms": Object { - "field": "slo.id", - }, - }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, - "slo.instanceId": Object { - "terms": Object { - "field": "slo.instanceId", - }, - }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.revision": Object { - "terms": Object { - "field": "slo.revision", - }, - }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, - "transaction.name": Object { - "terms": Object { - "field": "transaction.name", - "missing_bucket": true, - }, - }, - "transaction.type": Object { - "terms": Object { - "field": "transaction.type", - "missing_bucket": true, - }, - }, - }, - }, - "settings": Object { - "deduce_mappings": false, - "unattended": true, - }, - "source": Object { - "index": ".slo-observability.sli-v2*", - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "gte": "now-90d/m", - "lte": "now/m", - }, - }, - }, - Object { - "term": Object { - "slo.budgetingMethod": "timeslices", - }, - }, - Object { - "term": Object { - "slo.timeWindow.type": "rolling", - }, - }, - Object { - "term": Object { - "slo.timeWindow.duration": "90d", - }, - }, - ], - }, - }, - "runtime_mappings": Object { - "errorBudgetEstimated": Object { - "script": "emit(false)", - "type": "boolean", - }, - "isTempDoc": Object { - "script": "emit(false)", - "type": "boolean", - }, - }, - }, - "sync": Object { - "time": Object { - "delay": "125s", - "field": "@timestamp", - }, - }, - "transform_id": "slo-summary-timeslices-90d-rolling", - }, - Object { - "ignore": Array [ - 409, - ], - }, - ], - Array [ - Object { - "_meta": Object { - "managed": true, - "managed_by": "observability", - "version": 3, - }, - "description": "Summarize every SLO with timeslices budgeting method and a weekly calendar aligned time window", - "dest": Object { - "index": ".slo-observability.summary-v2", - "pipeline": ".slo-observability.summary.pipeline", - }, - "frequency": "1m", - "pivot": Object { - "aggregations": Object { - "_objectiveTarget": Object { - "max": Object { - "field": "slo.objective.target", - }, - }, - "_sliceDurationInSeconds": Object { - "max": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "_totalSlicesInPeriod": Object { - "bucket_script": Object { - "buckets_path": Object { - "sliceDurationInSeconds": "_sliceDurationInSeconds", - }, - "script": "Math.ceil(7 * 24 * 60 * 60 / params.sliceDurationInSeconds)", - }, - }, - "errorBudgetConsumed": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetInitial": "errorBudgetInitial", - "goodEvents": "goodEvents", - "totalEvents": "totalEvents", - "totalSlicesInPeriod": "_totalSlicesInPeriod", - }, - "script": "if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }", - }, - }, - "errorBudgetInitial": Object { - "bucket_script": Object { - "buckets_path": Object { - "objective": "_objectiveTarget", - }, - "script": "1 - params.objective", - }, - }, - "errorBudgetRemaining": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetConsumed": "errorBudgetConsumed", - }, - "script": "1 - params.errorBudgetConsumed", - }, - }, - "goodEvents": Object { - "sum": Object { - "field": "slo.isGoodSlice", - }, - }, - "sliValue": Object { - "bucket_script": Object { - "buckets_path": Object { - "goodEvents": "goodEvents", - "totalEvents": "totalEvents", - }, - "script": "if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }", - }, - }, - "statusCode": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetRemaining": "errorBudgetRemaining", - "objective": "_objectiveTarget", - "sliValue": "sliValue", - }, - "script": "if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }", - }, - }, - "totalEvents": Object { - "value_count": Object { - "field": "slo.isGoodSlice", - }, - }, - }, - "group_by": Object { - "errorBudgetEstimated": Object { - "terms": Object { - "field": "errorBudgetEstimated", - }, - }, - "isTempDoc": Object { - "terms": Object { - "field": "isTempDoc", - }, - }, - "service.environment": Object { - "terms": Object { - "field": "service.environment", - "missing_bucket": true, - }, - }, - "service.name": Object { - "terms": Object { - "field": "service.name", - "missing_bucket": true, - }, - }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, - "slo.id": Object { - "terms": Object { - "field": "slo.id", - }, - }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, - "slo.instanceId": Object { - "terms": Object { - "field": "slo.instanceId", - }, - }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.revision": Object { - "terms": Object { - "field": "slo.revision", - }, - }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, - "transaction.name": Object { - "terms": Object { - "field": "transaction.name", - "missing_bucket": true, - }, - }, - "transaction.type": Object { - "terms": Object { - "field": "transaction.type", - "missing_bucket": true, - }, - }, - }, - }, - "settings": Object { - "deduce_mappings": false, - "unattended": true, - }, - "source": Object { - "index": ".slo-observability.sli-v2*", - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "gte": "now/w", - "lte": "now/m", - }, - }, - }, - Object { - "term": Object { - "slo.budgetingMethod": "timeslices", - }, - }, - Object { - "term": Object { - "slo.timeWindow.type": "calendarAligned", - }, - }, - Object { - "term": Object { - "slo.timeWindow.duration": "1w", - }, - }, - ], - }, - }, - "runtime_mappings": Object { - "errorBudgetEstimated": Object { - "script": "emit(false)", - "type": "boolean", - }, - "isTempDoc": Object { - "script": "emit(false)", - "type": "boolean", - }, - }, - }, - "sync": Object { - "time": Object { - "delay": "125s", - "field": "@timestamp", - }, - }, - "transform_id": "slo-summary-timeslices-weekly-aligned", - }, - Object { - "ignore": Array [ - 409, - ], - }, - ], - Array [ - Object { - "_meta": Object { - "managed": true, - "managed_by": "observability", - "version": 3, - }, - "description": "Summarize every SLO with timeslices budgeting method and a monthly calendar aligned time window", - "dest": Object { - "index": ".slo-observability.summary-v2", - "pipeline": ".slo-observability.summary.pipeline", - }, - "frequency": "1m", - "pivot": Object { - "aggregations": Object { - "_objectiveTarget": Object { - "max": Object { - "field": "slo.objective.target", - }, - }, - "_sliceDurationInSeconds": Object { - "max": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "_totalSlicesInPeriod": Object { - "bucket_script": Object { - "buckets_path": Object { - "sliceDurationInSeconds": "_sliceDurationInSeconds", - }, - "script": Object { - "source": " - Date d = new Date(); - Instant instant = Instant.ofEpochMilli(d.getTime()); - LocalDateTime now = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); - LocalDateTime startOfMonth = now - .withDayOfMonth(1) - .withHour(0) - .withMinute(0) - .withSecond(0); - LocalDateTime startOfNextMonth = startOfMonth.plusMonths(1); - double sliceDurationInMinutes = params.sliceDurationInSeconds / 60; - - return Math.ceil(Duration.between(startOfMonth, startOfNextMonth).toMinutes() / sliceDurationInMinutes); - ", - }, - }, - }, - "errorBudgetConsumed": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetInitial": "errorBudgetInitial", - "goodEvents": "goodEvents", - "totalEvents": "totalEvents", - "totalSlicesInPeriod": "_totalSlicesInPeriod", - }, - "script": "if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }", - }, - }, - "errorBudgetInitial": Object { - "bucket_script": Object { - "buckets_path": Object { - "objective": "_objectiveTarget", - }, - "script": "1 - params.objective", - }, - }, - "errorBudgetRemaining": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetConsumed": "errorBudgetConsumed", - }, - "script": "1 - params.errorBudgetConsumed", - }, - }, - "goodEvents": Object { - "sum": Object { - "field": "slo.isGoodSlice", - }, - }, - "sliValue": Object { - "bucket_script": Object { - "buckets_path": Object { - "goodEvents": "goodEvents", - "totalEvents": "totalEvents", - }, - "script": "if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }", - }, - }, - "statusCode": Object { - "bucket_script": Object { - "buckets_path": Object { - "errorBudgetRemaining": "errorBudgetRemaining", - "objective": "_objectiveTarget", - "sliValue": "sliValue", - }, - "script": "if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }", - }, - }, - "totalEvents": Object { - "value_count": Object { - "field": "slo.isGoodSlice", - }, - }, - }, - "group_by": Object { - "errorBudgetEstimated": Object { - "terms": Object { - "field": "errorBudgetEstimated", - }, - }, - "isTempDoc": Object { - "terms": Object { - "field": "isTempDoc", - }, - }, - "service.environment": Object { - "terms": Object { - "field": "service.environment", - "missing_bucket": true, - }, - }, - "service.name": Object { - "terms": Object { - "field": "service.name", - "missing_bucket": true, - }, - }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, - "slo.id": Object { - "terms": Object { - "field": "slo.id", - }, - }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, - "slo.instanceId": Object { - "terms": Object { - "field": "slo.instanceId", - }, - }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.revision": Object { - "terms": Object { - "field": "slo.revision", - }, - }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, - "transaction.name": Object { - "terms": Object { - "field": "transaction.name", - "missing_bucket": true, - }, - }, - "transaction.type": Object { - "terms": Object { - "field": "transaction.type", - "missing_bucket": true, - }, - }, - }, - }, - "settings": Object { - "deduce_mappings": false, - "unattended": true, - }, - "source": Object { - "index": ".slo-observability.sli-v2*", - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "gte": "now/M", - "lte": "now/m", - }, - }, - }, - Object { - "term": Object { - "slo.budgetingMethod": "timeslices", - }, - }, - Object { - "term": Object { - "slo.timeWindow.type": "calendarAligned", - }, - }, - Object { - "term": Object { - "slo.timeWindow.duration": "1M", - }, - }, - ], - }, - }, - "runtime_mappings": Object { - "errorBudgetEstimated": Object { - "script": "emit(false)", - "type": "boolean", - }, - "isTempDoc": Object { - "script": "emit(false)", - "type": "boolean", - }, - }, - }, - "sync": Object { - "time": Object { - "delay": "125s", - "field": "@timestamp", - }, - }, - "transform_id": "slo-summary-timeslices-monthly-aligned", - }, - Object { - "ignore": Array [ - 409, - ], - }, - ], -] -`; diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.test.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.test.ts deleted file mode 100644 index be79f9d7961429..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { - ElasticsearchClientMock, - elasticsearchServiceMock, - loggingSystemMock, -} from '@kbn/core/server/mocks'; -import { MockedLogger } from '@kbn/logging-mocks'; -import { DefaultSummaryTransformInstaller } from './summary_transform_installer'; -import { ALL_TRANSFORM_TEMPLATES } from './templates'; - -describe('Summary Transform Installer', () => { - let esClientMock: ElasticsearchClientMock; - let loggerMock: jest.Mocked; - - beforeEach(() => { - esClientMock = elasticsearchServiceMock.createElasticsearchClient(); - loggerMock = loggingSystemMock.createLogger(); - }); - - it('skips the installation when latest version already installed', async () => { - esClientMock.transform.getTransform.mockResolvedValue({ - count: ALL_TRANSFORM_TEMPLATES.length, - // @ts-ignore - transforms: ALL_TRANSFORM_TEMPLATES.map((transform) => ({ - id: transform.transform_id, - _meta: transform._meta, - })), - }); - const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock); - - await installer.installAndStart(); - - expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled(); - expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled(); - expect(esClientMock.transform.putTransform).not.toHaveBeenCalled(); - expect(esClientMock.transform.startTransform).not.toHaveBeenCalled(); - }); - - it('installs every summary transforms when none are already installed', async () => { - esClientMock.transform.getTransform.mockResolvedValue({ count: 0, transforms: [] }); - const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock); - - await installer.installAndStart(); - - const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length; - - expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled(); - expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled(); - expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms); - expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms); - }); - - it('desinstalls previous summary transforms prior to installing the new ones', async () => { - esClientMock.transform.getTransform.mockResolvedValue({ - count: ALL_TRANSFORM_TEMPLATES.length, - // @ts-ignore - transforms: ALL_TRANSFORM_TEMPLATES.map((transform) => ({ - id: transform.transform_id, - _meta: { ...transform._meta, version: -1 }, - })), - }); - const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock); - - await installer.installAndStart(); - - const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length; - - expect(esClientMock.transform.stopTransform).toHaveBeenCalledTimes(nbOfTransforms); - expect(esClientMock.transform.deleteTransform).toHaveBeenCalledTimes(nbOfTransforms); - expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms); - expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms); - }); - - it('installs only the missing summary transforms', async () => { - const occurrencesSummaryTransforms = ALL_TRANSFORM_TEMPLATES.filter((transform) => - transform.transform_id.includes('-occurrences-') - ); - esClientMock.transform.getTransform.mockResolvedValue({ - count: occurrencesSummaryTransforms.length, - // @ts-ignore - transforms: occurrencesSummaryTransforms.map((transform) => ({ - id: transform.transform_id, - _meta: transform._meta, - })), - }); - const installer = new DefaultSummaryTransformInstaller(esClientMock, loggerMock); - - await installer.installAndStart(); - - const nbOfTransforms = ALL_TRANSFORM_TEMPLATES.length - occurrencesSummaryTransforms.length; - - expect(esClientMock.transform.stopTransform).not.toHaveBeenCalled(); - expect(esClientMock.transform.deleteTransform).not.toHaveBeenCalled(); - expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(nbOfTransforms); - expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(nbOfTransforms); - expect(esClientMock.transform.putTransform.mock.calls).toMatchSnapshot(); - }); -}); 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 deleted file mode 100644 index 0a51338615b42e..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../common/slo/constants'; -import { retryTransientEsErrors } from '../../../utils/retry'; -import { ALL_TRANSFORM_TEMPLATES } from './templates'; - -export interface SummaryTransformInstaller { - installAndStart(): Promise; -} - -export class DefaultSummaryTransformInstaller implements SummaryTransformInstaller { - constructor(private esClient: ElasticsearchClient, private logger: Logger) {} - - public async installAndStart(): Promise { - const allTransformIds = ALL_TRANSFORM_TEMPLATES.map((transform) => transform.transform_id); - const summaryTransforms = await this.execute(() => - this.esClient.transform.getTransform( - { transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`, allow_no_match: true }, - { ignore: [404] } - ) - ); - const alreadyInstalled = - summaryTransforms.count === allTransformIds.length && - summaryTransforms.transforms.every( - (transform) => transform._meta?.version === SLO_SUMMARY_TRANSFORMS_VERSION - ) && - summaryTransforms.transforms.every((transform) => allTransformIds.includes(transform.id)); - - if (alreadyInstalled) { - this.logger.info(`SLO summary transforms already installed - skipping`); - return; - } - - for (const transformTemplate of ALL_TRANSFORM_TEMPLATES) { - const transformId = transformTemplate.transform_id; - const transform = summaryTransforms.transforms.find((t) => t.id === transformId); - - const transformAlreadyInstalled = - !!transform && transform._meta?.version === SLO_SUMMARY_TRANSFORMS_VERSION; - const previousTransformAlreadyInstalled = - !!transform && transform._meta?.version !== SLO_SUMMARY_TRANSFORMS_VERSION; - - if (transformAlreadyInstalled) { - this.logger.info(`SLO summary transform [${transformId}] already installed - skipping`); - continue; - } - - if (previousTransformAlreadyInstalled) { - await this.deletePreviousTransformVersion(transformId); - } - - await this.installTransform(transformId, transformTemplate); - await this.startTransform(transformId); - } - - this.logger.info(`SLO summary transforms installed and started`); - } - - private async installTransform( - transformId: string, - transformTemplate: TransformPutTransformRequest - ) { - this.logger.info(`Installing SLO summary transform [${transformId}]`); - await this.execute(() => - this.esClient.transform.putTransform(transformTemplate, { ignore: [409] }) - ); - } - - private async deletePreviousTransformVersion(transformId: string) { - this.logger.info(`Deleting previous SLO summary transform [${transformId}]`); - await this.execute(() => - this.esClient.transform.stopTransform( - { transform_id: transformId, allow_no_match: true, force: true }, - { ignore: [409, 404] } - ) - ); - await this.execute(() => - this.esClient.transform.deleteTransform( - { transform_id: transformId, force: true }, - { ignore: [409, 404] } - ) - ); - } - - private async startTransform(transformId: string) { - this.logger.info(`Starting SLO summary transform [${transformId}]`); - await this.execute(() => - this.esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] }) - ); - } - - private async execute(esCall: () => Promise): Promise { - return await retryTransientEsErrors(esCall, { logger: this.logger }); - } -} diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/common.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/common.ts deleted file mode 100644 index c99a6c2be9d3cd..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/common.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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. - */ - -export const groupBy = { - 'slo.id': { - terms: { - field: 'slo.id', - }, - }, - 'slo.revision': { - terms: { - field: 'slo.revision', - }, - }, - 'slo.groupBy': { - terms: { - field: 'slo.groupBy', - }, - }, - 'slo.instanceId': { - terms: { - field: 'slo.instanceId', - }, - }, - 'slo.name': { - terms: { - field: 'slo.name', - }, - }, - 'slo.description': { - terms: { - field: 'slo.description', - }, - }, - 'slo.tags': { - terms: { - field: 'slo.tags', - }, - }, - 'slo.indicator.type': { - terms: { - field: 'slo.indicator.type', - }, - }, - 'slo.budgetingMethod': { - terms: { - field: 'slo.budgetingMethod', - }, - }, - 'slo.timeWindow.duration': { - terms: { - field: 'slo.timeWindow.duration', - }, - }, - 'slo.timeWindow.type': { - terms: { - field: 'slo.timeWindow.type', - }, - }, - errorBudgetEstimated: { - terms: { - field: 'errorBudgetEstimated', - }, - }, - // Differentiate the temporary document from the summary one - isTempDoc: { - terms: { - field: 'isTempDoc', - }, - }, - // optional fields: only specified for APM indicators. Must include missing_bucket:true - 'service.name': { - terms: { - field: 'service.name', - missing_bucket: true, - }, - }, - 'service.environment': { - terms: { - field: 'service.environment', - missing_bucket: true, - }, - }, - 'transaction.name': { - terms: { - field: 'transaction.name', - missing_bucket: true, - }, - }, - 'transaction.type': { - terms: { - field: 'transaction.type', - missing_bucket: true, - }, - }, -}; diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/index.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/index.ts deleted file mode 100644 index 68c42db91e923e..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { SUMMARY_OCCURRENCES_7D_ROLLING } from './summary_occurrences_7d_rolling'; -import { SUMMARY_OCCURRENCES_30D_ROLLING } from './summary_occurrences_30d_rolling'; -import { SUMMARY_OCCURRENCES_90D_ROLLING } from './summary_occurrences_90d_rolling'; -import { SUMMARY_TIMESLICES_7D_ROLLING } from './summary_timeslices_7d_rolling'; -import { SUMMARY_TIMESLICES_30D_ROLLING } from './summary_timeslices_30d_rolling'; -import { SUMMARY_TIMESLICES_90D_ROLLING } from './summary_timeslices_90d_rolling'; -import { SUMMARY_OCCURRENCES_WEEKLY_ALIGNED } from './summary_occurrences_weekly_aligned'; -import { SUMMARY_OCCURRENCES_MONTHLY_ALIGNED } from './summary_occurrences_monthly_aligned'; -import { SUMMARY_TIMESLICES_WEEKLY_ALIGNED } from './summary_timeslices_weekly_aligned'; -import { SUMMARY_TIMESLICES_MONTHLY_ALIGNED } from './summary_timeslices_monthly_aligned'; - -export const ALL_TRANSFORM_TEMPLATES = [ - SUMMARY_OCCURRENCES_7D_ROLLING, - SUMMARY_OCCURRENCES_30D_ROLLING, - SUMMARY_OCCURRENCES_90D_ROLLING, - SUMMARY_OCCURRENCES_WEEKLY_ALIGNED, - SUMMARY_OCCURRENCES_MONTHLY_ALIGNED, - SUMMARY_TIMESLICES_7D_ROLLING, - SUMMARY_TIMESLICES_30D_ROLLING, - SUMMARY_TIMESLICES_90D_ROLLING, - SUMMARY_TIMESLICES_WEEKLY_ALIGNED, - SUMMARY_TIMESLICES_MONTHLY_ALIGNED, -]; 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 deleted file mode 100644 index 9fb34a38051f7e..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_30d_rolling.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_OCCURRENCES_30D_ROLLING: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-30d-rolling`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-30d/m', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'occurrences', - }, - }, - { - term: { - 'slo.timeWindow.type': 'rolling', - }, - }, - { - term: { - 'slo.timeWindow.duration': '30d', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objectiveTarget: '_objectiveTarget', - }, - script: '1 - params.objectiveTarget', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsummed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsummed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objectiveTarget: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - }, - description: - 'Summarize every SLO with occurrences budgeting method and a 30 days rolling time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index 9ceb6cd4290a08..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_7d_rolling.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_OCCURRENCES_7D_ROLLING: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-7d-rolling`, - dest: { - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-7d/m', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'occurrences', - }, - }, - { - term: { - 'slo.timeWindow.type': 'rolling', - }, - }, - { - term: { - 'slo.timeWindow.duration': '7d', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objectiveTarget: '_objectiveTarget', - }, - script: '1 - params.objectiveTarget', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsummed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsummed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objectiveTarget: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - }, - description: - 'Summarize every SLO with occurrences budgeting method and a 7 days rolling time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index d0f1729f77225f..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_90d_rolling.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_OCCURRENCES_90D_ROLLING: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-90d-rolling`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-90d/m', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'occurrences', - }, - }, - { - term: { - 'slo.timeWindow.type': 'rolling', - }, - }, - { - term: { - 'slo.timeWindow.duration': '90d', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objectiveTarget: '_objectiveTarget', - }, - script: '1 - params.objectiveTarget', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsummed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsummed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objectiveTarget: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - }, - description: - 'Summarize every SLO with occurrences budgeting method and a 90 days rolling time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index a5b2a70932a5e6..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_monthly_aligned.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_OCCURRENCES_MONTHLY_ALIGNED: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-monthly-aligned`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(true)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now/M', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'occurrences', - }, - }, - { - term: { - 'slo.timeWindow.type': 'calendarAligned', - }, - }, - { - term: { - 'slo.timeWindow.duration': '1M', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objective: '_objectiveTarget', - }, - script: '1 - params.objective', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsumed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objective: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - description: - 'Summarize every SLO with occurrences budgeting method and a monthly calendar aligned time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index 43ed92704c1192..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_weekly_aligned.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_OCCURRENCES_WEEKLY_ALIGNED: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}occurrences-weekly-aligned`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(true)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now/w', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'occurrences', - }, - }, - { - term: { - 'slo.timeWindow.type': 'calendarAligned', - }, - }, - { - term: { - 'slo.timeWindow.duration': '1w', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objective: '_objectiveTarget', - }, - script: '1 - params.objective', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsumed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objective: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - description: - 'Summarize every SLO with occurrences budgeting method and a weekly calendar aligned time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index 5d1c6c48f8f1f1..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_30d_rolling.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_TIMESLICES_30D_ROLLING: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-30d-rolling`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-30d/m', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'timeslices', - }, - }, - { - term: { - 'slo.timeWindow.type': 'rolling', - }, - }, - { - term: { - 'slo.timeWindow.duration': '30d', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - goodEvents: { - sum: { - field: 'slo.isGoodSlice', - }, - }, - totalEvents: { - value_count: { - field: 'slo.isGoodSlice', - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objectiveTarget: '_objectiveTarget', - }, - script: '1 - params.objectiveTarget', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsummed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsummed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objectiveTarget: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - }, - description: - 'Summarize every SLO with timeslices budgeting method and a 30 days rolling time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index a9256955ac08a5..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_7d_rolling.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_TIMESLICES_7D_ROLLING: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-7d-rolling`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-7d/m', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'timeslices', - }, - }, - { - term: { - 'slo.timeWindow.type': 'rolling', - }, - }, - { - term: { - 'slo.timeWindow.duration': '7d', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - goodEvents: { - sum: { - field: 'slo.isGoodSlice', - }, - }, - totalEvents: { - value_count: { - field: 'slo.isGoodSlice', - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objectiveTarget: '_objectiveTarget', - }, - script: '1 - params.objectiveTarget', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsummed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsummed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objectiveTarget: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - }, - description: - 'Summarize every SLO with timeslices budgeting method and a 7 days rolling time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index f922bd210e253d..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_90d_rolling.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_TIMESLICES_90D_ROLLING: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-90d-rolling`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-90d/m', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'timeslices', - }, - }, - { - term: { - 'slo.timeWindow.type': 'rolling', - }, - }, - { - term: { - 'slo.timeWindow.duration': '90d', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - goodEvents: { - sum: { - field: 'slo.isGoodSlice', - }, - }, - totalEvents: { - value_count: { - field: 'slo.isGoodSlice', - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objectiveTarget: '_objectiveTarget', - }, - script: '1 - params.objectiveTarget', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsummed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsummed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objectiveTarget: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objectiveTarget) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - }, - description: - 'Summarize every SLO with timeslices budgeting method and a 90 days rolling time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index 3b39d9acd3372e..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_monthly_aligned.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_TIMESLICES_MONTHLY_ALIGNED: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-monthly-aligned`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now/M', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'timeslices', - }, - }, - { - term: { - 'slo.timeWindow.type': 'calendarAligned', - }, - }, - { - term: { - 'slo.timeWindow.duration': '1M', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - _sliceDurationInSeconds: { - max: { - field: 'slo.objective.sliceDurationInSeconds', - }, - }, - _totalSlicesInPeriod: { - bucket_script: { - buckets_path: { - sliceDurationInSeconds: '_sliceDurationInSeconds', - }, - script: { - source: ` - Date d = new Date(); - Instant instant = Instant.ofEpochMilli(d.getTime()); - LocalDateTime now = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); - LocalDateTime startOfMonth = now - .withDayOfMonth(1) - .withHour(0) - .withMinute(0) - .withSecond(0); - LocalDateTime startOfNextMonth = startOfMonth.plusMonths(1); - double sliceDurationInMinutes = params.sliceDurationInSeconds / 60; - - return Math.ceil(Duration.between(startOfMonth, startOfNextMonth).toMinutes() / sliceDurationInMinutes); - `, - }, - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - goodEvents: { - sum: { - field: 'slo.isGoodSlice', - }, - }, - totalEvents: { - value_count: { - field: 'slo.isGoodSlice', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objective: '_objectiveTarget', - }, - script: '1 - params.objective', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - totalSlicesInPeriod: '_totalSlicesInPeriod', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsumed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objective: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - description: - 'Summarize every SLO with timeslices budgeting method and a monthly calendar aligned time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - 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 deleted file mode 100644 index 3cae5f9bcd9b1f..00000000000000 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_weekly_aligned.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_NAME, - SLO_SUMMARY_INGEST_PIPELINE_NAME, - SLO_SUMMARY_TRANSFORMS_VERSION, - SLO_SUMMARY_TRANSFORM_NAME_PREFIX, -} from '../../../../../common/slo/constants'; -import { groupBy } from './common'; - -export const SUMMARY_TIMESLICES_WEEKLY_ALIGNED: TransformPutTransformRequest = { - transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}timeslices-weekly-aligned`, - dest: { - index: SLO_SUMMARY_DESTINATION_INDEX_NAME, - pipeline: SLO_SUMMARY_INGEST_PIPELINE_NAME, - }, - source: { - index: SLO_DESTINATION_INDEX_PATTERN, - runtime_mappings: { - errorBudgetEstimated: { - type: 'boolean', - script: 'emit(false)', - }, - isTempDoc: { - type: 'boolean', - script: 'emit(false)', - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now/w', - lte: 'now/m', - }, - }, - }, - { - term: { - 'slo.budgetingMethod': 'timeslices', - }, - }, - { - term: { - 'slo.timeWindow.type': 'calendarAligned', - }, - }, - { - term: { - 'slo.timeWindow.duration': '1w', - }, - }, - ], - }, - }, - }, - pivot: { - group_by: groupBy, - aggregations: { - _sliceDurationInSeconds: { - max: { - field: 'slo.objective.sliceDurationInSeconds', - }, - }, - _totalSlicesInPeriod: { - bucket_script: { - buckets_path: { - sliceDurationInSeconds: '_sliceDurationInSeconds', - }, - script: 'Math.ceil(7 * 24 * 60 * 60 / params.sliceDurationInSeconds)', - }, - }, - _objectiveTarget: { - max: { - field: 'slo.objective.target', - }, - }, - goodEvents: { - sum: { - field: 'slo.isGoodSlice', - }, - }, - totalEvents: { - value_count: { - field: 'slo.isGoodSlice', - }, - }, - sliValue: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { - bucket_script: { - buckets_path: { - objective: '_objectiveTarget', - }, - script: '1 - params.objective', - }, - }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - goodEvents: 'goodEvents', - totalEvents: 'totalEvents', - totalSlicesInPeriod: '_totalSlicesInPeriod', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { - errorBudgetConsumed: 'errorBudgetConsumed', - }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - objective: '_objectiveTarget', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= params.objective) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - }, - description: - 'Summarize every SLO with timeslices budgeting method and a weekly calendar aligned time window', - frequency: '1m', - sync: { - time: { - field: '@timestamp', - delay: '125s', - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - _meta: { - version: SLO_SUMMARY_TRANSFORMS_VERSION, - managed: true, - managed_by: 'observability', - }, -}; diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/common.ts b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/common.ts new file mode 100644 index 00000000000000..743ba333c8f989 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/common.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALL_VALUE } from '@kbn/slo-schema'; +import { SLO } from '../../../../domain/models/slo'; + +export const getGroupBy = (slo: SLO) => { + const groupings = + slo.groupBy !== '' && slo.groupBy !== ALL_VALUE + ? [slo.groupBy].flat().reduce((acc, field) => { + return { + ...acc, + [`slo.groupings.${field}`]: { + terms: { + field: `slo.groupings.${field}`, + }, + }, + }; + }, {}) + : {}; + + return { + 'slo.id': { + terms: { + field: 'slo.id', + }, + }, + 'slo.revision': { + terms: { + field: 'slo.revision', + }, + }, + 'slo.instanceId': { + terms: { + field: 'slo.instanceId', + }, + }, + ...groupings, + // optional fields: only specified for APM indicators. Must include missing_bucket:true + 'service.name': { + terms: { + field: 'service.name', + missing_bucket: true, + }, + }, + 'service.environment': { + terms: { + field: 'service.environment', + missing_bucket: true, + }, + }, + 'transaction.name': { + terms: { + field: 'transaction.name', + missing_bucket: true, + }, + }, + 'transaction.type': { + terms: { + field: 'transaction.type', + missing_bucket: true, + }, + }, + }; +}; diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/occurrences.ts b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/occurrences.ts new file mode 100644 index 00000000000000..2647e296f5544f --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/occurrences.ts @@ -0,0 +1,136 @@ +/* + * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { SLO } from '../../../../domain/models'; +import { + getSLOSummaryPipelineId, + getSLOSummaryTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, +} from '../../../../../common/slo/constants'; +import { getGroupBy } from './common'; + +export function generateSummaryTransformForOccurrences(slo: SLO): TransformPutTransformRequest { + return { + transform_id: getSLOSummaryTransformId(slo.id, slo.revision), + dest: { + pipeline: getSLOSummaryPipelineId(slo.id, slo.revision), + index: SLO_SUMMARY_DESTINATION_INDEX_NAME, + }, + source: { + index: SLO_DESTINATION_INDEX_PATTERN, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${slo.timeWindow.duration.format()}/m`, + lte: 'now/m', + }, + }, + }, + { + term: { + 'slo.id': slo.id, + }, + }, + { + term: { + 'slo.revision': slo.revision, + }, + }, + ], + }, + }, + }, + pivot: { + group_by: getGroupBy(slo), + aggregations: { + goodEvents: { + sum: { + field: 'slo.numerator', + }, + }, + totalEvents: { + sum: { + field: 'slo.denominator', + }, + }, + sliValue: { + bucket_script: { + buckets_path: { + goodEvents: 'goodEvents', + totalEvents: 'totalEvents', + }, + script: + 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', + }, + }, + errorBudgetInitial: { + bucket_script: { + buckets_path: {}, + script: `1 - ${slo.objective.target}`, + }, + }, + errorBudgetConsumed: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetInitial: 'errorBudgetInitial', + }, + script: + 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', + }, + }, + errorBudgetRemaining: { + bucket_script: { + buckets_path: { + errorBudgetConsumed: 'errorBudgetConsumed', + }, + script: '1 - params.errorBudgetConsumed', + }, + }, + statusCode: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetRemaining: 'errorBudgetRemaining', + }, + script: { + source: `if (params.sliValue == -1) { return 0 } else if (params.sliValue >= ${slo.objective.target}) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }`, + }, + }, + }, + latestSliTimestamp: { + max: { + field: '@timestamp', + }, + }, + }, + }, + description: `Summarise the rollup data of SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}].`, + frequency: '1m', + sync: { + time: { + field: 'event.ingested', + delay: '65s', + }, + }, + settings: { + deduce_mappings: false, + unattended: true, + }, + _meta: { + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, + }; +} diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/timeslices_calendar_aligned.ts b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/timeslices_calendar_aligned.ts new file mode 100644 index 00000000000000..6f5c27e5f869ad --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/timeslices_calendar_aligned.ts @@ -0,0 +1,166 @@ +/* + * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { DurationUnit, SLO } from '../../../../domain/models'; +import { + getSLOSummaryPipelineId, + getSLOSummaryTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, +} from '../../../../../common/slo/constants'; +import { getGroupBy } from './common'; + +export function generateSummaryTransformForTimeslicesAndCalendarAligned( + slo: SLO +): TransformPutTransformRequest { + const isWeeklyAligned = slo.timeWindow.duration.unit === DurationUnit.Week; + const sliceDurationInSeconds = slo.objective.timesliceWindow!.asSeconds(); + + return { + transform_id: getSLOSummaryTransformId(slo.id, slo.revision), + dest: { + pipeline: getSLOSummaryPipelineId(slo.id, slo.revision), + index: SLO_SUMMARY_DESTINATION_INDEX_NAME, + }, + source: { + index: SLO_DESTINATION_INDEX_PATTERN, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: isWeeklyAligned ? `now/w` : `now/M`, + lte: 'now/m', + }, + }, + }, + { + term: { + 'slo.id': slo.id, + }, + }, + { + term: { + 'slo.revision': slo.revision, + }, + }, + ], + }, + }, + }, + pivot: { + group_by: getGroupBy(slo), + aggregations: { + _totalSlicesInPeriod: { + bucket_script: { + buckets_path: {}, + script: { + source: ` + if (${isWeeklyAligned} == true) { + return Math.ceil(7 * 24 * 60 * 60 / ${sliceDurationInSeconds}); + } else { + Date d = new Date(); + Instant instant = Instant.ofEpochMilli(d.getTime()); + LocalDateTime now = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + LocalDateTime startOfMonth = now + .withDayOfMonth(1) + .withHour(0) + .withMinute(0) + .withSecond(0); + LocalDateTime startOfNextMonth = startOfMonth.plusMonths(1); + double sliceDurationInMinutes = ${sliceDurationInSeconds} / 60; + + return Math.ceil(Duration.between(startOfMonth, startOfNextMonth).toMinutes() / sliceDurationInMinutes); + } + `, + }, + }, + }, + goodEvents: { + sum: { + field: 'slo.isGoodSlice', + }, + }, + totalEvents: { + value_count: { + field: 'slo.isGoodSlice', + }, + }, + sliValue: { + bucket_script: { + buckets_path: { + goodEvents: 'goodEvents', + totalEvents: 'totalEvents', + }, + script: + 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', + }, + }, + errorBudgetInitial: { + bucket_script: { + buckets_path: {}, + script: `1 - ${slo.objective.target}`, + }, + }, + errorBudgetConsumed: { + bucket_script: { + buckets_path: { + goodEvents: 'goodEvents', + totalEvents: 'totalEvents', + totalSlicesInPeriod: '_totalSlicesInPeriod', + errorBudgetInitial: 'errorBudgetInitial', + }, + script: + 'if (params.totalEvents == 0) { return 0 } else { return (params.totalEvents - params.goodEvents) / (params.totalSlicesInPeriod * params.errorBudgetInitial) }', + }, + }, + errorBudgetRemaining: { + bucket_script: { + buckets_path: { + errorBudgetConsumed: 'errorBudgetConsumed', + }, + script: '1 - params.errorBudgetConsumed', + }, + }, + statusCode: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetRemaining: 'errorBudgetRemaining', + }, + script: `if (params.sliValue == -1) { return 0 } else if (params.sliValue >= ${slo.objective.target}) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }`, + }, + }, + latestSliTimestamp: { + max: { + field: '@timestamp', + }, + }, + }, + }, + description: `Summarise the rollup data of SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}].`, + frequency: '1m', + sync: { + time: { + field: 'event.ingested', + delay: '65s', + }, + }, + settings: { + deduce_mappings: false, + unattended: true, + }, + _meta: { + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, + }; +} diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/timeslices_rolling.ts b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/timeslices_rolling.ts new file mode 100644 index 00000000000000..a6212f671997b3 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/generators/timeslices_rolling.ts @@ -0,0 +1,138 @@ +/* + * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { SLO } from '../../../../domain/models'; +import { + getSLOSummaryPipelineId, + getSLOSummaryTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, +} from '../../../../../common/slo/constants'; +import { getGroupBy } from './common'; + +export function generateSummaryTransformForTimeslicesAndRolling( + slo: SLO +): TransformPutTransformRequest { + return { + transform_id: getSLOSummaryTransformId(slo.id, slo.revision), + dest: { + pipeline: getSLOSummaryPipelineId(slo.id, slo.revision), + index: SLO_SUMMARY_DESTINATION_INDEX_NAME, + }, + source: { + index: SLO_DESTINATION_INDEX_PATTERN, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${slo.timeWindow.duration.format()}/m`, + lte: 'now/m', + }, + }, + }, + { + term: { + 'slo.id': slo.id, + }, + }, + { + term: { + 'slo.revision': slo.revision, + }, + }, + ], + }, + }, + }, + pivot: { + group_by: getGroupBy(slo), + aggregations: { + goodEvents: { + sum: { + field: 'slo.isGoodSlice', + }, + }, + totalEvents: { + value_count: { + field: 'slo.isGoodSlice', + }, + }, + sliValue: { + bucket_script: { + buckets_path: { + goodEvents: 'goodEvents', + totalEvents: 'totalEvents', + }, + script: + 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', + }, + }, + errorBudgetInitial: { + bucket_script: { + buckets_path: {}, + script: `1 - ${slo.objective.target}`, + }, + }, + errorBudgetConsumed: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetInitial: 'errorBudgetInitial', + }, + script: + 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', + }, + }, + errorBudgetRemaining: { + bucket_script: { + buckets_path: { + errorBudgetConsumed: 'errorBudgetConsumed', + }, + script: '1 - params.errorBudgetConsumed', + }, + }, + statusCode: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetRemaining: 'errorBudgetRemaining', + }, + script: { + source: `if (params.sliValue == -1) { return 0 } else if (params.sliValue >= ${slo.objective.target}) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }`, + }, + }, + }, + latestSliTimestamp: { + max: { + field: '@timestamp', + }, + }, + }, + }, + description: `Summarise the rollup data of SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}].`, + frequency: '1m', + sync: { + time: { + field: 'event.ingested', + delay: '65s', + }, + }, + settings: { + deduce_mappings: false, + unattended: true, + }, + _meta: { + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, + }; +} diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform/helpers/create_temp_summary.ts b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/helpers/create_temp_summary.ts similarity index 80% rename from x-pack/plugins/observability/server/services/slo/summary_transform/helpers/create_temp_summary.ts rename to x-pack/plugins/observability/server/services/slo/summary_transform_generator/helpers/create_temp_summary.ts index 9b4a15f2bf51f0..166ca0198dbb81 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_transform/helpers/create_temp_summary.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/helpers/create_temp_summary.ts @@ -8,7 +8,7 @@ import { ALL_VALUE } from '@kbn/slo-schema'; import { SLO } from '../../../../domain/models'; -export function createTempSummaryDocument(slo: SLO) { +export function createTempSummaryDocument(slo: SLO, spaceId: string) { return { service: { environment: null, @@ -33,6 +33,11 @@ export function createTempSummaryDocument(slo: SLO) { id: slo.id, budgetingMethod: slo.budgetingMethod, revision: slo.revision, + objective: { + target: slo.objective.target, + timesliceTarget: slo.objective.timesliceTarget ?? null, + timesliceWindow: slo.objective.timesliceWindow?.format() ?? null, + }, tags: slo.tags, }, goodEvents: 0, @@ -45,5 +50,6 @@ export function createTempSummaryDocument(slo: SLO) { statusCode: 0, status: 'NO_DATA', isTempDoc: true, + spaceId, }; } diff --git a/x-pack/plugins/observability/server/services/slo/summary_transform_generator/summary_transform_generator.ts b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/summary_transform_generator.ts new file mode 100644 index 00000000000000..7710515f6538ad --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform_generator/summary_transform_generator.ts @@ -0,0 +1,30 @@ +/* + * 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 { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { SLO } from '../../../domain/models'; +import { generateSummaryTransformForOccurrences } from './generators/occurrences'; +import { generateSummaryTransformForTimeslicesAndRolling } from './generators/timeslices_rolling'; +import { generateSummaryTransformForTimeslicesAndCalendarAligned } from './generators/timeslices_calendar_aligned'; + +export interface SummaryTransformGenerator { + generate(slo: SLO): TransformPutTransformRequest; +} + +export class DefaultSummaryTransformGenerator implements SummaryTransformGenerator { + public generate(slo: SLO): TransformPutTransformRequest { + if (slo.budgetingMethod === 'occurrences') { + return generateSummaryTransformForOccurrences(slo); + } else if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'rolling') { + return generateSummaryTransformForTimeslicesAndRolling(slo); + } else if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'calendarAligned') { + return generateSummaryTransformForTimeslicesAndCalendarAligned(slo); + } + + throw new Error('Not supported SLO'); + } +} diff --git a/x-pack/plugins/observability/server/services/slo/summay_transform_manager.ts b/x-pack/plugins/observability/server/services/slo/summay_transform_manager.ts new file mode 100644 index 00000000000000..bc22f801c9fcc3 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summay_transform_manager.ts @@ -0,0 +1,99 @@ +/* + * 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 { ElasticsearchClient, Logger } from '@kbn/core/server'; + +import { SLO } from '../../domain/models'; +import { SecurityException } from '../../errors'; +import { retryTransientEsErrors } from '../../utils/retry'; +import { SummaryTransformGenerator } from './summary_transform_generator/summary_transform_generator'; +import { TransformManager } from './transform_manager'; + +type TransformId = string; + +export class DefaultSummaryTransformManager implements TransformManager { + constructor( + private generator: SummaryTransformGenerator, + private esClient: ElasticsearchClient, + private logger: Logger + ) {} + + async install(slo: SLO): Promise { + const transformParams = this.generator.generate(slo); + try { + await retryTransientEsErrors(() => this.esClient.transform.putTransform(transformParams), { + logger: this.logger, + }); + } catch (err) { + this.logger.error(`Cannot create summary transform for SLO [${slo.id}]`); + 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 summary transform [${transformId}]`); + throw err; + } + } + + async start(transformId: TransformId): Promise { + try { + await retryTransientEsErrors( + () => + this.esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] }), + { logger: this.logger } + ); + } catch (err) { + this.logger.error(`Cannot start SLO summary transform [${transformId}]`); + throw err; + } + } + + async stop(transformId: TransformId): Promise { + try { + await retryTransientEsErrors( + () => + this.esClient.transform.stopTransform( + { transform_id: transformId, wait_for_completion: true, force: true }, + { ignore: [404] } + ), + { logger: this.logger } + ); + } catch (err) { + this.logger.error(`Cannot stop SLO summary transform [${transformId}]`); + throw err; + } + } + + async uninstall(transformId: TransformId): Promise { + try { + await retryTransientEsErrors( + () => + this.esClient.transform.deleteTransform( + { transform_id: transformId, force: true }, + { ignore: [404] } + ), + { logger: this.logger } + ); + } catch (err) { + this.logger.error(`Cannot delete SLO summary transform [${transformId}]`); + throw err; + } + } +} 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 d5ac57c80e40d2..4fa12cae3e12e2 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 @@ -190,66 +190,21 @@ Object { "field": "service.environment", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, } `; @@ -304,66 +259,21 @@ Object { "field": "service.name", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, } `; @@ -413,66 +323,21 @@ Object { "fixed_interval": "1m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.name": Object { "terms": Object { "field": "transaction.name", @@ -527,66 +392,21 @@ Object { "fixed_interval": "1m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.type": Object { "terms": Object { "field": "transaction.type", @@ -600,12 +420,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -660,71 +480,21 @@ Object { "field": "service.name", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.sliceDurationInSeconds": Object { - "terms": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.name": Object { "terms": Object { "field": "transaction.name", @@ -794,33 +564,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('timeslices')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.apm.transactionDuration')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -830,48 +576,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.sliceDurationInSeconds": Object { - "script": Object { - "source": "emit(120)", - }, - "type": "long", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.98)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -880,7 +590,7 @@ Object { "field": "@timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; @@ -889,12 +599,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -940,66 +650,21 @@ Object { "field": "service.name", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.name": Object { "terms": Object { "field": "transaction.name", @@ -1069,33 +734,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('occurrences')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.apm.transactionDuration')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -1105,42 +746,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.999)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -1149,6 +760,6 @@ Object { "field": "@timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; 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 c8d687383b5139..515980a9dee54b 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 @@ -178,66 +178,21 @@ Object { "field": "service.environment", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, } `; @@ -288,66 +243,21 @@ Object { "field": "service.name", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, } `; @@ -393,66 +303,21 @@ Object { "fixed_interval": "1m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.name": Object { "terms": Object { "field": "transaction.name", @@ -503,66 +368,21 @@ Object { "fixed_interval": "1m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.type": Object { "terms": Object { "field": "transaction.type", @@ -576,12 +396,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -629,71 +449,21 @@ Object { "field": "service.name", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.sliceDurationInSeconds": Object { - "terms": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.name": Object { "terms": Object { "field": "transaction.name", @@ -759,33 +529,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('timeslices')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.apm.transactionErrorRate')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -795,48 +541,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.sliceDurationInSeconds": Object { - "script": Object { - "source": "emit(120)", - }, - "type": "long", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.98)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -845,7 +555,7 @@ Object { "field": "@timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; @@ -854,12 +564,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -898,66 +608,21 @@ Object { "field": "service.name", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, "transaction.name": Object { "terms": Object { "field": "transaction.name", @@ -1023,33 +688,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('occurrences')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.apm.transactionErrorRate')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -1059,42 +700,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.999)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -1103,6 +714,6 @@ Object { "field": "@timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; 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 cdbdd8c5270432..0b50f3ef5c52c6 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 @@ -77,12 +77,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -151,71 +151,21 @@ Object { "fixed_interval": "2m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.sliceDurationInSeconds": Object { - "terms": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -253,33 +203,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('timeslices')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.histogram.custom')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -289,48 +215,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.sliceDurationInSeconds": Object { - "script": Object { - "source": "emit(120)", - }, - "type": "long", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.98)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -339,7 +229,7 @@ Object { "field": "log_timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; @@ -348,12 +238,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -413,66 +303,21 @@ Object { "fixed_interval": "1m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -510,33 +355,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('occurrences')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.histogram.custom')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -546,42 +367,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.999)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -590,6 +381,6 @@ Object { "field": "log_timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; 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 27da87629465d2..0b5dc06be0a588 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 @@ -118,12 +118,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -166,71 +166,21 @@ Object { "fixed_interval": "2m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.sliceDurationInSeconds": Object { - "terms": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -268,33 +218,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('timeslices')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.kql.custom')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -304,48 +230,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.sliceDurationInSeconds": Object { - "script": Object { - "source": "emit(120)", - }, - "type": "long", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.98)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -354,7 +244,7 @@ Object { "field": "log_timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; @@ -363,12 +253,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -402,66 +292,21 @@ Object { "fixed_interval": "1m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -499,33 +344,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('occurrences')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.kql.custom')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -535,42 +356,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.999)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -579,6 +370,6 @@ Object { "field": "log_timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; 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 55ed414bd2ec7e..ed5ee454a5596b 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 @@ -117,12 +117,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -203,71 +203,21 @@ Object { "fixed_interval": "2m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.sliceDurationInSeconds": Object { - "terms": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -305,33 +255,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('timeslices')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.metric.custom')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -341,48 +267,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.sliceDurationInSeconds": Object { - "script": Object { - "source": "emit(120)", - }, - "type": "long", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.98)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -391,7 +281,7 @@ Object { "field": "log_timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; @@ -400,12 +290,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -477,66 +367,21 @@ Object { "fixed_interval": "1m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -574,33 +419,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('occurrences')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.metric.custom')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -610,42 +431,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.999)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -654,7 +445,7 @@ Object { "field": "log_timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/timeslice_metric.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/timeslice_metric.test.ts.snap index e2698ba3e17936..48ebb4de1e8d60 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/timeslice_metric.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/timeslice_metric.test.ts.snap @@ -33,12 +33,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -173,71 +173,21 @@ Object { "fixed_interval": "2m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.sliceDurationInSeconds": Object { - "terms": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -272,33 +222,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('timeslices')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.metric.timeslice')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -308,48 +234,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.sliceDurationInSeconds": Object { - "script": Object { - "source": "emit(120)", - }, - "type": "long", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.98)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -358,7 +248,7 @@ Object { "field": "@timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; @@ -367,12 +257,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 2, + "version": 3, }, - "description": "Rolled-up SLI data for SLO: irrelevant", + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", "dest": Object { - "index": ".slo-observability.sli-v2", - "pipeline": ".slo-observability.sli.pipeline", + "index": ".slo-observability.sli-v3", + "pipeline": ".slo-observability.sli.pipeline-v3", }, "frequency": "1m", "pivot": Object { @@ -507,71 +397,21 @@ Object { "fixed_interval": "2m", }, }, - "slo.budgetingMethod": Object { - "terms": Object { - "field": "slo.budgetingMethod", - }, - }, - "slo.description": Object { - "terms": Object { - "field": "slo.description", - }, - }, - "slo.groupBy": Object { - "terms": Object { - "field": "slo.groupBy", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", }, }, - "slo.indicator.type": Object { - "terms": Object { - "field": "slo.indicator.type", - }, - }, "slo.instanceId": Object { "terms": Object { "field": "slo.instanceId", }, }, - "slo.name": Object { - "terms": Object { - "field": "slo.name", - }, - }, - "slo.objective.sliceDurationInSeconds": Object { - "terms": Object { - "field": "slo.objective.sliceDurationInSeconds", - }, - }, - "slo.objective.target": Object { - "terms": Object { - "field": "slo.objective.target", - }, - }, "slo.revision": Object { "terms": Object { "field": "slo.revision", }, }, - "slo.tags": Object { - "terms": Object { - "field": "slo.tags", - }, - }, - "slo.timeWindow.duration": Object { - "terms": Object { - "field": "slo.timeWindow.duration", - }, - }, - "slo.timeWindow.type": Object { - "terms": Object { - "field": "slo.timeWindow.type", - }, - }, }, }, "settings": Object { @@ -606,33 +446,9 @@ Object { }, }, "runtime_mappings": Object { - "slo.budgetingMethod": Object { - "script": Object { - "source": "emit('timeslices')", - }, - "type": "keyword", - }, - "slo.description": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.groupBy": Object { - "script": Object { - "source": "emit('*')", - }, - "type": "keyword", - }, "slo.id": Object { "script": Object { - "source": Any, - }, - "type": "keyword", - }, - "slo.indicator.type": Object { - "script": Object { - "source": "emit('sli.metric.timeslice')", + "source": "emit('irrelevant')", }, "type": "keyword", }, @@ -642,48 +458,12 @@ Object { }, "type": "keyword", }, - "slo.name": Object { - "script": Object { - "source": "emit('irrelevant')", - }, - "type": "keyword", - }, - "slo.objective.sliceDurationInSeconds": Object { - "script": Object { - "source": "emit(120)", - }, - "type": "long", - }, - "slo.objective.target": Object { - "script": Object { - "source": "emit(0.98)", - }, - "type": "double", - }, "slo.revision": Object { "script": Object { "source": "emit(1)", }, "type": "long", }, - "slo.tags": Object { - "script": Object { - "source": "emit('critical,k8s')", - }, - "type": "keyword", - }, - "slo.timeWindow.duration": Object { - "script": Object { - "source": "emit('7d')", - }, - "type": "keyword", - }, - "slo.timeWindow.type": Object { - "script": Object { - "source": "emit('rolling')", - }, - "type": "keyword", - }, }, }, "sync": Object { @@ -692,6 +472,6 @@ Object { "field": "@timestamp", }, }, - "transform_id": Any, + "transform_id": "slo-irrelevant-1", } `; diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts index f19ee083a3115f..6b5491e4871912 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts @@ -17,32 +17,20 @@ const generator = new ApmTransactionDurationTransformGenerator(); describe('APM Transaction Duration Transform Generator', () => { it('returns the expected transform params with every specified indicator params', () => { - const slo = createSLO({ indicator: createAPMTransactionDurationIndicator() }); + const slo = createSLO({ id: 'irrelevant', indicator: createAPMTransactionDurationIndicator() }); const transform = generator.getTransformParams(slo); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); - expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`); - expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${slo.id}')` }, - }); - expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${slo.revision})` }, - }); + expect(transform).toMatchSnapshot(); }); it('returns the expected transform params for timeslices slo', () => { const slo = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', indicator: createAPMTransactionDurationIndicator(), }); const transform = generator.getTransformParams(slo); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); + expect(transform).toMatchSnapshot(); }); it("does not include the query filter when params are '*'", () => { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts index 54f72f19615880..9934f5ea27a517 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts @@ -17,32 +17,23 @@ const generator = new ApmTransactionErrorRateTransformGenerator(); describe('APM Transaction Error Rate Transform Generator', () => { it('returns the expected transform params with every specified indicator params', async () => { - const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() }); + const slo = createSLO({ + id: 'irrelevant', + indicator: createAPMTransactionErrorRateIndicator(), + }); const transform = generator.getTransformParams(slo); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); - expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`); - expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${slo.id}')` }, - }); - expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${slo.revision})` }, - }); + expect(transform).toMatchSnapshot(); }); it('returns the expected transform params for timeslices slo', async () => { const slo = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', indicator: createAPMTransactionErrorRateIndicator(), }); const transform = generator.getTransformParams(slo); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); + expect(transform).toMatchSnapshot(); }); it("does not include the query filter when params are '*'", async () => { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.test.ts index fdf0bc27563fe3..77ef70e75ca5f9 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.test.ts @@ -30,6 +30,7 @@ describe('Histogram Transform Generator', () => { }); expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); }); + it('throws when the total filter is invalid', () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ @@ -42,6 +43,7 @@ describe('Histogram Transform Generator', () => { }); expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); }); + it('throws when the query_filter is invalid', () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ filter: '{ kql.query: invalid' }), @@ -51,32 +53,20 @@ describe('Histogram Transform Generator', () => { }); it('returns the expected transform params with every specified indicator params', async () => { - const anSLO = createSLO({ indicator: createHistogramIndicator() }); + const anSLO = createSLO({ id: 'irrelevant', indicator: createHistogramIndicator() }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); - expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`); - expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${anSLO.id}')` }, - }); - expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${anSLO.revision})` }, - }); + expect(transform).toMatchSnapshot(); }); it('returns the expected transform params for timeslices slo', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', indicator: createHistogramIndicator(), }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); + expect(transform).toMatchSnapshot(); }); it('filters the source using the kql query', async () => { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.test.ts index dd4511ec44bfd9..1512bb5a655ab3 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.test.ts @@ -37,32 +37,20 @@ describe('KQL Custom Transform Generator', () => { }); it('returns the expected transform params with every specified indicator params', async () => { - const anSLO = createSLO({ indicator: createKQLCustomIndicator() }); + const anSLO = createSLO({ id: 'irrelevant', indicator: createKQLCustomIndicator() }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); - expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`); - expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${anSLO.id}')` }, - }); - expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${anSLO.revision})` }, - }); + expect(transform).toMatchSnapshot(); }); it('returns the expected transform params for timeslices slo', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', indicator: createKQLCustomIndicator(), }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); + expect(transform).toMatchSnapshot(); }); it('filters the source using the kql query', async () => { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts index 69685bad0c09e3..9ebacde28f0ed1 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts @@ -69,32 +69,20 @@ describe('Metric Custom Transform Generator', () => { }); it('returns the expected transform params with every specified indicator params', async () => { - const anSLO = createSLO({ indicator: createMetricCustomIndicator() }); + const anSLO = createSLO({ id: 'irrelevant', indicator: createMetricCustomIndicator() }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); - expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`); - expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${anSLO.id}')` }, - }); - expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${anSLO.revision})` }, - }); + expect(transform).toMatchSnapshot(); }); it('returns the expected transform params for timeslices slo', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', indicator: createMetricCustomIndicator(), }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); + expect(transform).toMatchSnapshot(); }); it('filters the source using the kql query', async () => { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/timeslice_metric.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/timeslice_metric.test.ts index aa21f7e0ceb0e4..7d221c25c27ede 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/timeslice_metric.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/timeslice_metric.test.ts @@ -70,33 +70,22 @@ describe('Timeslice Metric Transform Generator', () => { it('returns the expected transform params with every specified indicator params', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', indicator: everythingIndicator, }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); - expect(transform.transform_id).toEqual(`slo-${anSLO.id}-${anSLO.revision}`); - expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${anSLO.id}')` }, - }); - expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${anSLO.revision})` }, - }); + expect(transform).toMatchSnapshot(); }); it('returns the expected transform params for timeslices slo', async () => { const anSLO = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', indicator: everythingIndicator, }); const transform = generator.getTransformParams(anSLO); - expect(transform).toMatchSnapshot({ - transform_id: expect.any(String), - source: { runtime_mappings: { 'slo.id': { script: { source: expect.any(String) } } } }, - }); + expect(transform).toMatchSnapshot(); }); it('filters the source using the kql query', async () => { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts index 42572e61b38abb..7085f69b764224 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts @@ -32,12 +32,6 @@ export abstract class TransformGenerator { source: `emit(${slo.revision})`, }, }, - 'slo.groupBy': { - type: 'keyword', - script: { - source: `emit('${!!slo.groupBy ? slo.groupBy : ALL_VALUE}')`, - }, - }, ...(mustIncludeAllInstanceId && { 'slo.instanceId': { type: 'keyword', @@ -46,67 +40,11 @@ export abstract class TransformGenerator { }, }, }), - 'slo.name': { - type: 'keyword', - script: { - source: `emit('${slo.name}')`, - }, - }, - 'slo.description': { - type: 'keyword', - script: { - source: `emit('${slo.description}')`, - }, - }, - 'slo.tags': { - type: 'keyword', - script: { - source: `emit('${slo.tags}')`, - }, - }, - 'slo.indicator.type': { - type: 'keyword', - script: { - source: `emit('${slo.indicator.type}')`, - }, - }, - 'slo.objective.target': { - type: 'double', - script: { - source: `emit(${slo.objective.target})`, - }, - }, - ...(slo.objective.timesliceWindow && { - 'slo.objective.sliceDurationInSeconds': { - type: 'long', - script: { - source: `emit(${slo.objective.timesliceWindow!.asSeconds()})`, - }, - }, - }), - 'slo.budgetingMethod': { - type: 'keyword', - script: { - source: `emit('${slo.budgetingMethod}')`, - }, - }, - 'slo.timeWindow.duration': { - type: 'keyword', - script: { - source: `emit('${slo.timeWindow.duration.format()}')`, - }, - }, - 'slo.timeWindow.type': { - type: 'keyword', - script: { - source: `emit('${slo.timeWindow.type}')`, - }, - }, }; } public buildDescription(slo: SLO): string { - return `Rolled-up SLI data for SLO: ${slo.name}`; + return `Rolled-up SLI data for SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}]`; } public buildCommonGroupBy( @@ -119,27 +57,27 @@ export abstract class TransformGenerator { fixedInterval = slo.objective.timesliceWindow!.format(); } - const instanceIdField = - slo.groupBy !== '' && slo.groupBy !== ALL_VALUE ? slo.groupBy : 'slo.instanceId'; + const groupings = + slo.groupBy !== '' && slo.groupBy !== ALL_VALUE + ? [slo.groupBy].flat().reduce( + (acc, field) => { + return { + ...acc, + [`slo.groupings.${field}`]: { + terms: { + field, + }, + }, + }; + }, + { 'slo.instanceId': { terms: { field: slo.groupBy } } } + ) + : { 'slo.instanceId': { terms: { field: 'slo.instanceId' } } }; return { 'slo.id': { terms: { field: 'slo.id' } }, 'slo.revision': { terms: { field: 'slo.revision' } }, - 'slo.groupBy': { terms: { field: 'slo.groupBy' } }, - 'slo.instanceId': { terms: { field: instanceIdField } }, - 'slo.name': { terms: { field: 'slo.name' } }, - 'slo.description': { terms: { field: 'slo.description' } }, - 'slo.tags': { terms: { field: 'slo.tags' } }, - 'slo.indicator.type': { terms: { field: 'slo.indicator.type' } }, - 'slo.objective.target': { terms: { field: 'slo.objective.target' } }, - ...(slo.objective.timesliceWindow && { - 'slo.objective.sliceDurationInSeconds': { - terms: { field: 'slo.objective.sliceDurationInSeconds' }, - }, - }), - 'slo.budgetingMethod': { terms: { field: 'slo.budgetingMethod' } }, - 'slo.timeWindow.duration': { terms: { field: 'slo.timeWindow.duration' } }, - 'slo.timeWindow.type': { terms: { field: 'slo.timeWindow.type' } }, + ...groupings, ...extraGroupByFields, // @timestamp field defined in the destination index '@timestamp': { 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 1fb6550e12c479..a8642cfa921f26 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 @@ -6,11 +6,13 @@ */ import { ElasticsearchClient } from '@kbn/core/server'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { MockedLogger } from '@kbn/logging-mocks'; import { UpdateSLOParams } from '@kbn/slo-schema'; -import { cloneDeep, pick, omit } from 'lodash'; +import { cloneDeep, omit, pick } from 'lodash'; import { + getSLOSummaryTransformId, getSLOTransformId, SLO_DESTINATION_INDEX_PATTERN, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, @@ -22,7 +24,12 @@ import { createSLO, createSLOWithTimeslicesBudgetingMethod, } from './fixtures/slo'; -import { createSLORepositoryMock, createTransformManagerMock } from './mocks'; +import { weeklyCalendarAligned } from './fixtures/time_window'; +import { + createSLORepositoryMock, + createSummaryTransformManagerMock, + createTransformManagerMock, +} from './mocks'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; import { UpdateSLO } from './update_slo'; @@ -31,13 +38,24 @@ describe('UpdateSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; let mockEsClient: jest.Mocked; + let loggerMock: jest.Mocked; + let mockSummaryTransformManager: jest.Mocked; let updateSLO: UpdateSLO; beforeEach(() => { mockRepository = createSLORepositoryMock(); mockTransformManager = createTransformManagerMock(); + loggerMock = loggingSystemMock.createLogger(); + mockSummaryTransformManager = createSummaryTransformManagerMock(); mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); - updateSLO = new UpdateSLO(mockRepository, mockTransformManager, mockEsClient); + updateSLO = new UpdateSLO( + mockRepository, + mockTransformManager, + mockSummaryTransformManager, + mockEsClient, + loggerMock, + 'some-space' + ); }); describe('when the update payload does not change the original SLO', () => { @@ -45,12 +63,18 @@ describe('UpdateSLO', () => { expect(mockTransformManager.stop).not.toBeCalled(); expect(mockTransformManager.uninstall).not.toBeCalled(); expect(mockTransformManager.install).not.toBeCalled(); - expect(mockTransformManager.preview).not.toBeCalled(); expect(mockTransformManager.start).not.toBeCalled(); + + expect(mockSummaryTransformManager.stop).not.toBeCalled(); + expect(mockSummaryTransformManager.uninstall).not.toBeCalled(); + expect(mockSummaryTransformManager.install).not.toBeCalled(); + expect(mockSummaryTransformManager.start).not.toBeCalled(); + expect(mockEsClient.deleteByQuery).not.toBeCalled(); + expect(mockEsClient.ingest.putPipeline).not.toBeCalled(); } - it('returns early with a full identical SLO payload', async () => { + it('returns early with a fully identical SLO payload', async () => { const slo = createSLO(); mockRepository.findById.mockResolvedValueOnce(slo); const updatePayload: UpdateSLOParams = omit(cloneDeep(slo), [ @@ -58,6 +82,7 @@ describe('UpdateSLO', () => { 'revision', 'createdAt', 'updatedAt', + 'version', 'enabled', ]); @@ -157,111 +182,116 @@ describe('UpdateSLO', () => { }); }); - it('updates the settings correctly', async () => { - const slo = createSLO(); - mockRepository.findById.mockResolvedValueOnce(slo); + describe('handles breaking changes', () => { + it('consideres a settings change as a breaking change', async () => { + const slo = createSLO(); + mockRepository.findById.mockResolvedValueOnce(slo); - const newSettings = { ...slo.settings, timestamp_field: 'newField' }; - await updateSLO.execute(slo.id, { settings: newSettings }); + const newSettings = { ...slo.settings, timestamp_field: 'newField' }; + await updateSLO.execute(slo.id, { settings: newSettings }); + + expectDeletionOfOriginalSLOResources(slo); + expect(mockRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + ...slo, + settings: newSettings, + revision: 2, + updatedAt: expect.anything(), + }) + ); + expectInstallationOfUpdatedSLOResources(); + }); - expectDeletionOfOriginalSLO(slo); - expect(mockRepository.save).toBeCalledWith( - expect.objectContaining({ - ...slo, - settings: newSettings, - revision: 2, - updatedAt: expect.anything(), - }) - ); - expectInstallationOfNewSLOTransform(); - }); + it('consideres a budgeting method change as a breaking change', async () => { + const slo = createSLO({ budgetingMethod: 'occurrences' }); + mockRepository.findById.mockResolvedValueOnce(slo); - it('updates the budgeting method correctly', async () => { - const slo = createSLO({ budgetingMethod: 'occurrences' }); - mockRepository.findById.mockResolvedValueOnce(slo); - - await updateSLO.execute(slo.id, { - budgetingMethod: 'timeslices', - objective: { - target: slo.objective.target, - timesliceTarget: 0.9, - timesliceWindow: oneMinute(), - }, + await updateSLO.execute(slo.id, { + budgetingMethod: 'timeslices', + objective: { + target: slo.objective.target, + timesliceTarget: 0.9, + timesliceWindow: oneMinute(), + }, + }); + + expectInstallationOfUpdatedSLOResources(); + expectDeletionOfOriginalSLOResources(slo); }); - expectDeletionOfOriginalSLO(slo); - expectInstallationOfNewSLOTransform(); - }); + it('consideres a timeWindow change as a breaking change', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod(); + mockRepository.findById.mockResolvedValueOnce(slo); - it('updates the timeslice target correctly', async () => { - const slo = createSLOWithTimeslicesBudgetingMethod(); - mockRepository.findById.mockResolvedValueOnce(slo); + await updateSLO.execute(slo.id, { + timeWindow: weeklyCalendarAligned(), + }); - await updateSLO.execute(slo.id, { - objective: { - target: slo.objective.target, - timesliceTarget: 0.1, - timesliceWindow: slo.objective.timesliceWindow, - }, + expectInstallationOfUpdatedSLOResources(); + expectDeletionOfOriginalSLOResources(slo); }); - expectDeletionOfOriginalSLO(slo); - expectInstallationOfNewSLOTransform(); - }); + it('consideres a timeslice target change as a breaking change', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod(); + mockRepository.findById.mockResolvedValueOnce(slo); - it('consideres a timeslice window change as a breaking change', async () => { - const slo = createSLOWithTimeslicesBudgetingMethod(); - mockRepository.findById.mockResolvedValueOnce(slo); + await updateSLO.execute(slo.id, { + objective: { + target: slo.objective.target, + timesliceTarget: 0.1, + timesliceWindow: slo.objective.timesliceWindow, + }, + }); - await updateSLO.execute(slo.id, { - objective: { - target: slo.objective.target, - timesliceTarget: slo.objective.timesliceTarget, - timesliceWindow: fiveMinute(), - }, + expectInstallationOfUpdatedSLOResources(); + expectDeletionOfOriginalSLOResources(slo); }); - expectDeletionOfOriginalSLO(slo); - expectInstallationOfNewSLOTransform(); - }); + it('consideres a timeslice window change as a breaking change', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod(); + mockRepository.findById.mockResolvedValueOnce(slo); - it('index a temporary summary document', async () => { - const slo = createSLO({ - id: 'unique-id', - indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), + await updateSLO.execute(slo.id, { + objective: { + target: slo.objective.target, + timesliceTarget: slo.objective.timesliceTarget, + timesliceWindow: fiveMinute(), + }, + }); + + expectInstallationOfUpdatedSLOResources(); + expectDeletionOfOriginalSLOResources(slo); }); - mockRepository.findById.mockResolvedValueOnce(slo); - const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); - await updateSLO.execute(slo.id, { indicator: newIndicator }); + it('consideres an indicator change as a breaking change', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod(); + mockRepository.findById.mockResolvedValueOnce(slo); - expect(mockEsClient.index.mock.calls[0]).toMatchSnapshot(); - }); + await updateSLO.execute(slo.id, { + indicator: createAPMTransactionErrorRateIndicator(), + }); - it('removes the original data from the original SLO', async () => { - const slo = createSLO({ - indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), + expectInstallationOfUpdatedSLOResources(); + expectDeletionOfOriginalSLOResources(slo); }); - mockRepository.findById.mockResolvedValueOnce(slo); - const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); - await updateSLO.execute(slo.id, { indicator: newIndicator }); + it('consideres a groupBy change as a breaking change', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod(); + mockRepository.findById.mockResolvedValueOnce(slo); - expect(mockRepository.save).toBeCalledWith( - expect.objectContaining({ - ...slo, - indicator: newIndicator, - revision: 2, - updatedAt: expect.anything(), - }) - ); - expectInstallationOfNewSLOTransform(); - expectDeletionOfOriginalSLO(slo); + await updateSLO.execute(slo.id, { + groupBy: 'new-field', + }); + + expectInstallationOfUpdatedSLOResources(); + expectDeletionOfOriginalSLOResources(slo); + }); }); - describe('when error happens during the transform installation step', () => { + describe('when error happens during the update', () => { it('restores the previous SLO definition in the repository', async () => { const slo = createSLO({ + id: 'original-id', indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), }); mockRepository.findById.mockResolvedValueOnce(slo); @@ -274,47 +304,38 @@ describe('UpdateSLO', () => { ); 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(); + + // these calls are related to the updated slo + expect(mockSummaryTransformManager.stop).toMatchSnapshot(); + expect(mockSummaryTransformManager.uninstall).toMatchSnapshot(); + expect(mockTransformManager.stop).toMatchSnapshot(); + expect(mockTransformManager.uninstall).toMatchSnapshot(); + expect(mockEsClient.ingest.deletePipeline).toMatchSnapshot(); }); }); - 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' }); + function expectInstallationOfUpdatedSLOResources() { + expect(mockTransformManager.install).toHaveBeenCalled(); + expect(mockTransformManager.start).toHaveBeenCalled(); - await expect(updateSLO.execute(slo.id, { indicator: newIndicator })).rejects.toThrowError( - 'Transform start error' - ); + expect(mockEsClient.ingest.putPipeline).toHaveBeenCalled(); - 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(); - }); - }); + expect(mockSummaryTransformManager.install).toHaveBeenCalled(); + expect(mockSummaryTransformManager.start).toHaveBeenCalled(); - function expectInstallationOfNewSLOTransform() { - expect(mockTransformManager.install).toBeCalled(); - expect(mockTransformManager.preview).toBeCalled(); - expect(mockTransformManager.start).toBeCalled(); + expect(mockEsClient.index).toHaveBeenCalled(); } - function expectDeletionOfOriginalSLO(originalSlo: SLO) { + function expectDeletionOfOriginalSLOResources(originalSlo: SLO) { const transformId = getSLOTransformId(originalSlo.id, originalSlo.revision); - expect(mockTransformManager.stop).toBeCalledWith(transformId); - expect(mockTransformManager.uninstall).toBeCalledWith(transformId); + expect(mockTransformManager.stop).toHaveBeenCalledWith(transformId); + expect(mockTransformManager.uninstall).toHaveBeenCalledWith(transformId); + + const summaryTransformId = getSLOSummaryTransformId(originalSlo.id, originalSlo.revision); + expect(mockSummaryTransformManager.stop).toHaveBeenCalledWith(summaryTransformId); + expect(mockSummaryTransformManager.uninstall).toHaveBeenCalledWith(summaryTransformId); + + expect(mockEsClient.ingest.deletePipeline).toHaveBeenCalled(); expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2); expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith( 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 0d039c93c1ab08..1a73d54decbeee 100644 --- a/x-pack/plugins/observability/server/services/slo/update_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/update_slo.ts @@ -5,26 +5,33 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { UpdateSLOParams, UpdateSLOResponse, updateSLOResponseSchema } from '@kbn/slo-schema'; -import { isEqual } from 'lodash'; +import { isEqual, pick } from 'lodash'; import { + getSLOSummaryPipelineId, + getSLOSummaryTransformId, getSLOTransformId, SLO_DESTINATION_INDEX_PATTERN, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../../common/slo/constants'; +import { getSLOSummaryPipelineTemplate } from '../../assets/ingest_templates/slo_summary_pipeline_template'; import { SLO } from '../../domain/models'; import { validateSLO } from '../../domain/services'; +import { retryTransientEsErrors } from '../../utils/retry'; import { SLORepository } from './slo_repository'; -import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary'; +import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; export class UpdateSLO { constructor( private repository: SLORepository, private transformManager: TransformManager, - private esClient: ElasticsearchClient + private summaryTransformManager: TransformManager, + private esClient: ElasticsearchClient, + private logger: Logger, + private spaceId: string ) {} public async execute(sloId: string, params: UpdateSLOParams): Promise { @@ -37,42 +44,81 @@ export class UpdateSLO { return this.toResponse(originalSlo); } + const fields = [ + 'indicator', + 'groupBy', + 'timeWindow', + 'objective', + 'budgetingMethod', + 'settings', + ]; + const requireRevisionBump = !isEqual(pick(originalSlo, fields), pick(updatedSlo, fields)); + updatedSlo = Object.assign(updatedSlo, { updatedAt: new Date(), - revision: originalSlo.revision + 1, + revision: requireRevisionBump ? originalSlo.revision + 1 : originalSlo.revision, }); validateSLO(updatedSlo); - - const updatedSloTransformId = getSLOTransformId(updatedSlo.id, updatedSlo.revision); await this.repository.save(updatedSlo); - try { - await this.transformManager.install(updatedSlo); - } catch (err) { - await this.repository.save(originalSlo); - throw err; + if (!requireRevisionBump) { + // At this point, we still need to update the summary pipeline to include the changes (name, desc, tags, ...) in the summary index + await retryTransientEsErrors( + () => + this.esClient.ingest.putPipeline(getSLOSummaryPipelineTemplate(updatedSlo, this.spaceId)), + { logger: this.logger } + ); + + return this.toResponse(updatedSlo); } + const updatedRollupTransformId = getSLOTransformId(updatedSlo.id, updatedSlo.revision); + const updatedSummaryTransformId = getSLOSummaryTransformId(updatedSlo.id, updatedSlo.revision); + try { - await this.transformManager.preview(updatedSloTransformId); - await this.transformManager.start(updatedSloTransformId); + await this.transformManager.install(updatedSlo); + await this.transformManager.start(updatedRollupTransformId); + + await retryTransientEsErrors( + () => + this.esClient.ingest.putPipeline(getSLOSummaryPipelineTemplate(updatedSlo, this.spaceId)), + { logger: this.logger } + ); + + await this.summaryTransformManager.install(updatedSlo); + await this.summaryTransformManager.start(updatedSummaryTransformId); + + await retryTransientEsErrors( + () => + this.esClient.index({ + index: SLO_SUMMARY_TEMP_INDEX_NAME, + id: `slo-${updatedSlo.id}`, + document: createTempSummaryDocument(updatedSlo, this.spaceId), + refresh: true, + }), + { logger: this.logger } + ); } catch (err) { - await Promise.all([ - this.transformManager.uninstall(updatedSloTransformId), - this.repository.save(originalSlo), - ]); + this.logger.error( + `Cannot update the SLO [id: ${updatedSlo.id}, revision: ${updatedSlo.revision}]. Rolling back.` + ); + + // Restore the previous slo definition + await this.repository.save(originalSlo); + // delete the created resources for the updated slo + await this.summaryTransformManager.stop(updatedSummaryTransformId); + await this.summaryTransformManager.uninstall(updatedSummaryTransformId); + await this.transformManager.stop(updatedRollupTransformId); + await this.transformManager.uninstall(updatedRollupTransformId); + await this.esClient.ingest.deletePipeline( + { id: getSLOSummaryPipelineId(updatedSlo.id, updatedSlo.revision) }, + { ignore: [404] } + ); throw err; } - await this.esClient.index({ - index: SLO_SUMMARY_TEMP_INDEX_NAME, - id: `slo-${updatedSlo.id}`, - document: createTempSummaryDocument(updatedSlo), - refresh: true, - }); - await this.deleteOriginalSLO(originalSlo); return this.toResponse(updatedSlo); @@ -80,9 +126,21 @@ export class UpdateSLO { private async deleteOriginalSLO(originalSlo: SLO) { try { - const originalSloTransformId = getSLOTransformId(originalSlo.id, originalSlo.revision); - await this.transformManager.stop(originalSloTransformId); - await this.transformManager.uninstall(originalSloTransformId); + const originalRollupTransformId = getSLOTransformId(originalSlo.id, originalSlo.revision); + await this.transformManager.stop(originalRollupTransformId); + await this.transformManager.uninstall(originalRollupTransformId); + + const originalSummaryTransformId = getSLOSummaryTransformId( + originalSlo.id, + originalSlo.revision + ); + await this.summaryTransformManager.stop(originalSummaryTransformId); + await this.summaryTransformManager.uninstall(originalSummaryTransformId); + + await this.esClient.ingest.deletePipeline( + { id: getSLOSummaryPipelineId(originalSlo.id, originalSlo.revision) }, + { ignore: [404] } + ); } catch (err) { // Any errors here should not prevent moving forward. // Worst case we keep rolling up data for the previous revision number.