From 757c881b9a845dd438c16f1eca915c1e522fcd5d Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Mon, 31 Jul 2023 20:13:38 -0400 Subject: [PATCH] feat(slo): introduce new search capabilities (#162665) --- .../src/models/duration.test.ts | 14 +- .../kbn-slo-schema/src/models/duration.ts | 14 +- .../kbn-slo-schema/src/rest_specs/slo.ts | 19 +- .../kbn-slo-schema/src/schema/common.ts | 4 - .../docs/openapi/slo/bundled.json | 35 +- .../docs/openapi/slo/bundled.yaml | 30 +- .../slo/paths/s@{spaceid}@api@slos.yaml | 22 +- .../slo_selector.test.tsx | 4 +- .../burn_rate_rule_editor/slo_selector.tsx | 2 +- .../slo/slo_status_badge/slo_status_badge.tsx | 17 +- .../public/hooks/slo/query_key_factory.ts | 4 +- .../public/hooks/slo/use_clone_slo.ts | 3 - .../public/hooks/slo/use_create_slo.ts | 5 +- .../public/hooks/slo/use_delete_slo.ts | 3 - .../public/hooks/slo/use_fetch_slo_list.ts | 21 +- .../badges/slo_time_window_badge.tsx | 8 +- .../public/pages/slos/components/slo_list.tsx | 39 +- .../pages/slos/components/slo_list_item.tsx | 43 +- .../pages/slos/components/slo_list_items.tsx | 15 +- ...lo_list_search_filter_sort_bar.stories.tsx | 1 - .../slo_list_search_filter_sort_bar.tsx | 170 +-- .../public/pages/slos/slos.test.tsx | 22 + .../observability/public/pages/slos/slos.tsx | 3 +- .../observability/public/typings/slo/index.ts | 2 +- .../public/utils/slo/duration.ts | 17 +- .../observability/public/utils/slo/labels.ts | 11 - .../slo_mappings_template.ts | 79 +- .../slo_settings_template.ts | 4 +- .../slo_summary_mappings_template.ts | 128 ++ .../slo_summary_settings_template.ts | 23 + .../observability/server/assets/constants.ts | 27 +- .../index_templates/slo_index_templates.ts | 4 +- .../slo_summary_index_templates.ts | 25 + .../ingest_templates/slo_pipeline_template.ts | 4 +- .../slo_summary_pipeline_template.ts | 60 + .../slo_transform_template.ts | 3 +- .../server/domain/models/time_window.ts | 36 +- .../domain/services/compute_burn_rate.test.ts | 12 +- .../server/domain/services/date_range.test.ts | 48 +- .../server/domain/services/date_range.ts | 21 +- .../domain/services/validate_slo.test.ts | 55 +- .../server/domain/services/validate_slo.ts | 9 +- x-pack/plugins/observability/server/plugin.ts | 62 +- .../observability/server/routes/slo/route.ts | 35 +- .../__snapshots__/summary_client.test.ts.snap | 4 +- .../slo/__snapshots__/create_slo.test.ts.snap | 46 + .../summary_search_client.test.ts.snap | 106 ++ .../slo/__snapshots__/update_slo.test.ts.snap | 49 + .../server/services/slo/create_slo.test.ts | 28 +- .../server/services/slo/create_slo.ts | 20 +- .../server/services/slo/delete_slo.test.ts | 25 +- .../server/services/slo/delete_slo.ts | 21 +- .../server/services/slo/find_slo.test.ts | 196 +-- .../server/services/slo/find_slo.ts | 64 +- .../server/services/slo/fixtures/duration.ts | 8 +- .../server/services/slo/fixtures/slo.ts | 30 +- .../slo/fixtures/summary_search_document.ts | 71 + .../services/slo/fixtures/time_window.ts | 31 +- .../server/services/slo/get_diagnosis.ts | 78 +- .../server/services/slo/index.ts | 2 + .../server/services/slo/mocks/index.ts | 17 +- .../services/slo/resource_installer.test.ts | 73 +- .../server/services/slo/resource_installer.ts | 155 ++- .../server/services/slo/sli_client.ts | 25 +- .../server/services/slo/slo_installer.test.ts | 34 + .../server/services/slo/slo_installer.ts | 46 + .../services/slo/slo_repository.test.ts | 265 +--- .../server/services/slo/slo_repository.ts | 129 +- .../slo/summary_search_client.test.ts | 116 ++ .../services/slo/summary_search_client.ts | 157 +++ .../summary_transform_installer.test.ts.snap | 1162 +++++++++++++++++ .../helpers/create_temp_summary.ts | 47 + .../summary_transform_installer.test.ts | 103 ++ .../summary_transform_installer.ts | 105 ++ .../slo/summary_transform/templates/common.ts | 95 ++ .../slo/summary_transform/templates/index.ts | 30 + .../summary_occurrences_30d_rolling.ts | 153 +++ .../summary_occurrences_7d_rolling.ts | 152 +++ .../summary_occurrences_90d_rolling.ts | 152 +++ .../summary_occurrences_monthly_aligned.ts | 150 +++ .../summary_occurrences_weekly_aligned.ts | 150 +++ .../summary_timeslices_30d_rolling.ts | 152 +++ .../summary_timeslices_7d_rolling.ts | 152 +++ .../summary_timeslices_90d_rolling.ts | 152 +++ .../summary_timeslices_monthly_aligned.ts | 180 +++ .../summary_timeslices_weekly_aligned.ts | 165 +++ .../apm_transaction_duration.test.ts.snap | 697 +++++++++- .../apm_transaction_error_rate.test.ts.snap | 681 +++++++++- .../__snapshots__/histogram.test.ts.snap | 221 +++- .../__snapshots__/kql_custom.test.ts.snap | 221 +++- .../__snapshots__/metric_custom.test.ts.snap | 221 +++- .../apm_transaction_duration.test.ts | 100 +- .../apm_transaction_duration.ts | 36 +- .../apm_transaction_error_rate.test.ts | 90 +- .../apm_transaction_error_rate.ts | 34 +- .../slo/transform_generators/histogram.ts | 2 +- .../slo/transform_generators/kql_custom.ts | 2 +- .../slo/transform_generators/metric_custom.ts | 2 +- .../transform_generator.ts | 110 +- .../server/services/slo/update_slo.test.ts | 218 ++-- .../server/services/slo/update_slo.ts | 84 +- .../translations/translations/fr-FR.json | 5 - .../translations/translations/ja-JP.json | 6 +- .../translations/translations/zh-CN.json | 5 - 104 files changed, 7385 insertions(+), 1409 deletions(-) create mode 100644 x-pack/plugins/observability/server/assets/component_templates/slo_summary_mappings_template.ts create mode 100644 x-pack/plugins/observability/server/assets/component_templates/slo_summary_settings_template.ts create mode 100644 x-pack/plugins/observability/server/assets/index_templates/slo_summary_index_templates.ts create mode 100644 x-pack/plugins/observability/server/assets/ingest_templates/slo_summary_pipeline_template.ts create mode 100644 x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap create mode 100644 x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap create mode 100644 x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap create mode 100644 x-pack/plugins/observability/server/services/slo/fixtures/summary_search_document.ts create mode 100644 x-pack/plugins/observability/server/services/slo/slo_installer.test.ts create mode 100644 x-pack/plugins/observability/server/services/slo/slo_installer.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_search_client.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/__snapshots__/summary_transform_installer.test.ts.snap create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/helpers/create_temp_summary.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.test.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/common.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/index.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_30d_rolling.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_7d_rolling.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_90d_rolling.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_monthly_aligned.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_weekly_aligned.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_30d_rolling.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_7d_rolling.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_90d_rolling.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_monthly_aligned.ts create mode 100644 x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_weekly_aligned.ts diff --git a/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts b/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts index 7a7e6ebfec99fc..5e1f0e14a5dc3d 100644 --- a/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts +++ b/x-pack/packages/kbn-slo-schema/src/models/duration.test.ts @@ -27,8 +27,6 @@ describe('Duration', () => { expect(new Duration(1, DurationUnit.Day).format()).toBe('1d'); expect(new Duration(1, DurationUnit.Week).format()).toBe('1w'); expect(new Duration(1, DurationUnit.Month).format()).toBe('1M'); - expect(new Duration(1, DurationUnit.Quarter).format()).toBe('1Q'); - expect(new Duration(1, DurationUnit.Year).format()).toBe('1Y'); }); }); @@ -39,31 +37,25 @@ describe('Duration', () => { expect(short.isShorterThan(new Duration(1, DurationUnit.Day))).toBe(true); expect(short.isShorterThan(new Duration(1, DurationUnit.Week))).toBe(true); expect(short.isShorterThan(new Duration(1, DurationUnit.Month))).toBe(true); - expect(short.isShorterThan(new Duration(1, DurationUnit.Quarter))).toBe(true); - expect(short.isShorterThan(new Duration(1, DurationUnit.Year))).toBe(true); }); it('returns false when the current duration is longer (or equal) than the other duration', () => { - const long = new Duration(1, DurationUnit.Year); + const long = new Duration(1, DurationUnit.Month); expect(long.isShorterThan(new Duration(1, DurationUnit.Minute))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Hour))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Day))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Week))).toBe(false); expect(long.isShorterThan(new Duration(1, DurationUnit.Month))).toBe(false); - expect(long.isShorterThan(new Duration(1, DurationUnit.Quarter))).toBe(false); - expect(long.isShorterThan(new Duration(1, DurationUnit.Year))).toBe(false); }); }); describe('isLongerOrEqualThan', () => { it('returns true when the current duration is longer or equal than the other duration', () => { - const long = new Duration(2, DurationUnit.Year); + const long = new Duration(2, DurationUnit.Month); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Hour))).toBe(true); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(true); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(true); expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(true); - expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(true); - expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(true); }); it('returns false when the current duration is shorter than the other duration', () => { @@ -73,8 +65,6 @@ describe('Duration', () => { expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(false); expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(false); expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(false); - expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(false); - expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(false); }); }); diff --git a/x-pack/packages/kbn-slo-schema/src/models/duration.ts b/x-pack/packages/kbn-slo-schema/src/models/duration.ts index d4016a3389b2ad..33ff6cbd25ac8e 100644 --- a/x-pack/packages/kbn-slo-schema/src/models/duration.ts +++ b/x-pack/packages/kbn-slo-schema/src/models/duration.ts @@ -14,8 +14,6 @@ enum DurationUnit { 'Day' = 'd', 'Week' = 'w', 'Month' = 'M', - 'Quarter' = 'Q', - 'Year' = 'Y', } class Duration { @@ -55,6 +53,10 @@ class Duration { format(): string { return `${this.value}${this.unit}`; } + + asSeconds(): number { + return moment.duration(this.value, toMomentUnitOfTime(this.unit)).asSeconds(); + } } const toDurationUnit = (unit: string): DurationUnit => { @@ -69,10 +71,6 @@ const toDurationUnit = (unit: string): DurationUnit => { return DurationUnit.Week; case 'M': return DurationUnit.Month; - case 'Q': - return DurationUnit.Quarter; - case 'y': - return DurationUnit.Year; default: throw new Error('invalid duration unit'); } @@ -90,10 +88,6 @@ const toMomentUnitOfTime = (unit: DurationUnit): moment.unitOfTime.Diff => { return 'weeks'; case DurationUnit.Month: return 'months'; - case DurationUnit.Quarter: - return 'quarters'; - case DurationUnit.Year: - return 'years'; default: assertNever(unit); } 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 fda0e767c71d0e..1a398ca25bbf0b 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 @@ -7,15 +7,17 @@ import * as t from 'io-ts'; import { + apmTransactionDurationIndicatorSchema, + apmTransactionErrorRateIndicatorSchema, budgetingMethodSchema, dateType, + durationType, + histogramIndicatorSchema, historicalSummarySchema, indicatorSchema, - indicatorTypesArraySchema, indicatorTypesSchema, kqlCustomIndicatorSchema, metricCustomIndicatorSchema, - histogramIndicatorSchema, objectiveSchema, optionalSettingsSchema, previewDataSchema, @@ -24,9 +26,6 @@ import { summarySchema, tagsSchema, timeWindowSchema, - apmTransactionErrorRateIndicatorSchema, - apmTransactionDurationIndicatorSchema, - durationType, timeWindowTypeSchema, } from '../schema'; @@ -69,12 +68,16 @@ const getSLOParamsSchema = t.type({ }); const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]); -const sortBySchema = t.union([t.literal('creationTime'), t.literal('indicatorType')]); +const sortBySchema = t.union([ + t.literal('error_budget_consumed'), + t.literal('error_budget_remaining'), + t.literal('sli_value'), + t.literal('status'), +]); const findSLOParamsSchema = t.partial({ query: t.partial({ - name: t.string, - indicatorTypes: indicatorTypesArraySchema, + kqlQuery: t.string, page: t.string, perPage: t.string, sortBy: sortBySchema, diff --git a/x-pack/packages/kbn-slo-schema/src/schema/common.ts b/x-pack/packages/kbn-slo-schema/src/schema/common.ts index 250525ce2192c1..166f3eab34a924 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/common.ts @@ -43,8 +43,6 @@ const summarySchema = t.type({ errorBudget: errorBudgetSchema, }); -type SummarySchema = t.TypeOf; - const historicalSummarySchema = t.intersection([ t.type({ date: dateType, @@ -59,8 +57,6 @@ const previewDataSchema = t.type({ const dateRangeSchema = t.type({ from: dateType, to: dateType }); -export type { SummarySchema }; - export { ALL_VALUE, allOrAnyString, diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.json b/x-pack/plugins/observability/docs/openapi/slo/bundled.json index 05437b3e890283..2e9cec07f5e0ba 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.json +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.json @@ -136,27 +136,13 @@ "$ref": "#/components/parameters/space_id" }, { - "name": "name", + "name": "kqlQuery", "in": "query", - "description": "Filter by name", + "description": "A valid kql query to filter the SLO with", "schema": { "type": "string" }, - "example": "awesome-service" - }, - { - "name": "indicatorTypes", - "in": "query", - "description": "Filter by indicator type", - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "example": [ - "sli.kql.custom" - ] + "example": "slo.name:latency* and slo.tags : \"prod\"" }, { "name": "page", @@ -176,7 +162,7 @@ "type": "integer", "default": 25 }, - "example": 20 + "example": 25 }, { "name": "sortBy", @@ -185,12 +171,14 @@ "schema": { "type": "string", "enum": [ - "creationTime", - "indicatorType" + "sli_value", + "status", + "error_budget_consumed", + "error_budget_remaining" ], - "default": "creationTime" + "default": "status" }, - "example": "creationTime" + "example": "status" }, { "name": "sortDirection", @@ -1333,7 +1321,8 @@ "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.apm.sli.metric.custom": "#/components/schemas/indicator_properties_custom_metric" + "sli.metric.custom": "#/components/schemas/indicator_properties_custom_metric", + "sli.histogram.custom": "#/components/schemas/indicator_properties_histogram" } }, "oneOf": [ diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml index 624c641aca5d37..075877796b31ba 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml @@ -80,21 +80,12 @@ paths: parameters: - $ref: '#/components/parameters/kbn_xsrf' - $ref: '#/components/parameters/space_id' - - name: name + - name: kqlQuery in: query - description: Filter by name + description: A valid kql query to filter the SLO with schema: type: string - example: awesome-service - - name: indicatorTypes - in: query - description: Filter by indicator type - schema: - type: array - items: - type: string - example: - - sli.kql.custom + example: 'slo.name:latency* and slo.tags : "prod"' - name: page in: query description: The page number to return @@ -108,17 +99,19 @@ paths: schema: type: integer default: 25 - example: 20 + example: 25 - name: sortBy in: query description: Sort by field schema: type: string enum: - - creationTime - - indicatorType - default: creationTime - example: creationTime + - sli_value + - status + - error_budget_consumed + - error_budget_remaining + default: status + example: status - name: sortDirection in: query description: Sort order @@ -920,7 +913,8 @@ components: 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.apm.sli.metric.custom: '#/components/schemas/indicator_properties_custom_metric' + sli.metric.custom: '#/components/schemas/indicator_properties_custom_metric' + sli.histogram.custom: '#/components/schemas/indicator_properties_histogram' oneOf: - $ref: '#/components/schemas/indicator_properties_custom_kql' - $ref: '#/components/schemas/indicator_properties_apm_availability' 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 36ac4cb27e3b7a..0c7559e41bb62e 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 @@ -60,20 +60,12 @@ get: parameters: - $ref: ../components/headers/kbn_xsrf.yaml - $ref: ../components/parameters/space_id.yaml - - name: name + - name: kqlQuery in: query - description: Filter by name + description: A valid kql query to filter the SLO with schema: type: string - example: awesome-service - - name: indicatorTypes - in: query - description: Filter by indicator type - schema: - type: array - items: - type: string - example: ['sli.kql.custom'] + example: 'slo.name:latency* and slo.tags : "prod"' - name: page in: query description: The page number to return @@ -87,15 +79,15 @@ get: schema: type: integer default: 25 - example: 20 + example: 25 - name: sortBy in: query description: Sort by field schema: type: string - enum: [creationTime, indicatorType] - default: creationTime - example: creationTime + enum: [sli_value, status, error_budget_consumed, error_budget_remaining] + default: status + example: status - name: sortDirection in: query description: Sort order diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx index 9c40a23b02d466..12d837fafaba2d 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx @@ -30,7 +30,7 @@ describe('SLO Selector', () => { render(); expect(screen.getByTestId('sloSelector')).toBeTruthy(); - expect(useFetchSloListMock).toHaveBeenCalledWith({ name: '' }); + expect(useFetchSloListMock).toHaveBeenCalledWith({ kqlQuery: 'slo.name:*' }); }); it('searches SLOs when typing', async () => { @@ -42,6 +42,6 @@ describe('SLO Selector', () => { await wait(310); // debounce delay }); - expect(useFetchSloListMock).toHaveBeenCalledWith({ name: 'latency' }); + expect(useFetchSloListMock).toHaveBeenCalledWith({ kqlQuery: 'slo.name:latency*' }); }); }); 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 4e92a864fa9986..4dd13c92fcfa33 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 @@ -23,7 +23,7 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) { const [options, setOptions] = useState>>([]); const [selectedOptions, setSelectedOptions] = useState>>(); const [searchValue, setSearchValue] = useState(''); - const { isLoading, sloList } = useFetchSloList({ name: searchValue }); + const { isLoading, sloList } = useFetchSloList({ kqlQuery: `slo.name:${searchValue}*` }); const hasError = errors !== undefined && errors.length > 0; useEffect(() => { diff --git a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.tsx b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.tsx index 5872921fa652b0..5cb35d88ac77d4 100644 --- a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.tsx +++ b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiBadge, EuiFlexItem } from '@elastic/eui'; +import { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; @@ -19,11 +19,18 @@ export function SloStatusBadge({ slo }: SloStatusProps) { <> {slo.summary.status === 'NO_DATA' && ( - - {i18n.translate('xpack.observability.slo.sloStatusBadge.noData', { - defaultMessage: 'No data', + + > + + {i18n.translate('xpack.observability.slo.sloStatusBadge.noData', { + defaultMessage: 'No data', + })} + + )} {slo.summary.status === 'HEALTHY' && ( diff --git a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts index 28cf100d97bc90..d0c893df2d2a00 100644 --- a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts +++ b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts @@ -8,10 +8,10 @@ import type { Indicator } from '@kbn/slo-schema'; interface SloListFilter { - name: string; + kqlQuery: string; page: number; sortBy: string; - indicatorTypes: string[]; + sortDirection: string; } interface CompositeSloKeyFilter { diff --git a/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts index 95fa7928ae2991..08a3c0520ca181 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_clone_slo.ts @@ -77,9 +77,6 @@ export function useCloneSlo() { }) ); }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); - }, } ); } diff --git a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts index bcb97c6668f840..8debbc4b9eefd4 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts @@ -44,7 +44,7 @@ export function useCreateSlo() { const [queryKey, previousData] = queriesData?.at(0) ?? []; - const newItem = { ...slo, id: uuidv1() }; + const newItem = { ...slo, id: uuidv1(), summary: undefined }; const optimisticUpdate = { page: previousData?.page ?? 1, @@ -83,9 +83,6 @@ export function useCreateSlo() { http.basePath.prepend(paths.observability.sloCreateWithEncodedForm(encode(slo))) ); }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); - }, } ); } diff --git a/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts index 9da12e73979f60..7add4684426771 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts @@ -76,9 +76,6 @@ export function useDeleteSlo() { }) ); }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); - }, } ); } diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts index f2d1627114a0d2..22a918eb231279 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts @@ -20,10 +20,10 @@ import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; interface SLOListParams { - name?: string; + kqlQuery?: string; page?: number; sortBy?: string; - indicatorTypes?: string[]; + sortDirection?: 'asc' | 'desc'; shouldRefetch?: boolean; } @@ -43,10 +43,10 @@ const SHORT_REFETCH_INTERVAL = 1000 * 5; // 5 seconds const LONG_REFETCH_INTERVAL = 1000 * 60; // 1 minute export function useFetchSloList({ - name = '', + kqlQuery = '', page = 1, - sortBy = 'creationTime', - indicatorTypes = [], + sortBy = 'status', + sortDirection = 'desc', shouldRefetch, }: SLOListParams | undefined = {}): UseFetchSloListResponse { const { @@ -61,18 +61,15 @@ export function useFetchSloList({ const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery( { - queryKey: sloKeys.list({ name, page, sortBy, indicatorTypes }), + queryKey: sloKeys.list({ kqlQuery, page, sortBy, sortDirection }), queryFn: async ({ signal }) => { try { const response = await http.get(`/api/observability/slos`, { query: { - ...(page && { page }), - ...(name && { name }), + ...(kqlQuery && { kqlQuery }), ...(sortBy && { sortBy }), - ...(indicatorTypes && - indicatorTypes.length > 0 && { - indicatorTypes: indicatorTypes.join(','), - }), + ...(sortDirection && { sortDirection }), + ...(page && { page }), }, signal, }); diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx index ac52e9f36bf764..d218eeda7f0ed2 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx @@ -11,7 +11,7 @@ import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-sc import { euiLightVars } from '@kbn/ui-theme'; import moment from 'moment'; import React from 'react'; -import { toMomentUnitOfTime } from '../../../../utils/slo/duration'; +import { toCalendarAlignedMomentUnitOfTime } from '../../../../utils/slo/duration'; import { toDurationLabel } from '../../../../utils/slo/labels'; export interface Props { @@ -34,11 +34,11 @@ export function SloTimeWindowBadge({ slo }: Props) { ); } - const unitMoment = toMomentUnitOfTime(unit); + const unitMoment = toCalendarAlignedMomentUnitOfTime(unit); const now = moment.utc(); - const periodStart = now.clone().startOf(unitMoment!); - const periodEnd = now.clone().endOf(unitMoment!); + const periodStart = now.clone().startOf(unitMoment); + const periodEnd = now.clone().endOf(unitMoment); const totalDurationInDays = periodEnd.diff(periodStart, 'days') + 1; const elapsedDurationInDays = now.diff(periodStart, 'days') + 1; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index 0efab27b0ebdf7..9e1db8c019381f 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -5,18 +5,12 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui'; -import { debounce } from 'lodash'; import { useIsMutating } from '@tanstack/react-query'; - +import React, { useState } from 'react'; import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list'; -import { - FilterType, - SloListSearchFilterSortBar, - SortType, -} from './slo_list_search_filter_sort_bar'; import { SloListItems } from './slo_list_items'; +import { SloListSearchFilterSortBar, SortField } from './slo_list_search_filter_sort_bar'; export interface Props { autoRefresh: boolean; @@ -24,16 +18,14 @@ export interface Props { export function SloList({ autoRefresh }: Props) { const [activePage, setActivePage] = useState(0); - const [query, setQuery] = useState(''); - const [sort, setSort] = useState('creationTime'); - const [indicatorTypeFilter, setIndicatorTypeFilter] = useState([]); + const [sort, setSort] = useState('status'); const { isInitialLoading, isLoading, isRefetching, isError, sloList, refetch } = useFetchSloList({ page: activePage + 1, - name: query, + kqlQuery: query, sortBy: sort, - indicatorTypes: indicatorTypeFilter, + sortDirection: 'desc', shouldRefetch: autoRefresh, }); @@ -49,20 +41,16 @@ export function SloList({ autoRefresh }: Props) { refetch(); }; - const handleChangeQuery = useMemo( - () => - debounce((e: React.ChangeEvent) => { - setQuery(e.target.value); - }, 300), - [] - ); - - const handleChangeSort = (newSort: SortType) => { - setSort(newSort); + const handleChangeQuery = (newQuery: string) => { + setActivePage(0); + setQuery(newQuery); + refetch(); }; - const handleChangeIndicatorTypeFilter = (newFilter: FilterType[]) => { - setIndicatorTypeFilter(newFilter); + const handleChangeSort = (newSort: SortField | undefined) => { + setActivePage(0); + setSort(newSort); + refetch(); }; return ( @@ -80,7 +68,6 @@ export function SloList({ autoRefresh }: Props) { } onChangeQuery={handleChangeQuery} onChangeSort={handleChangeSort} - onChangeIndicatorTypeFilter={handleChangeIndicatorTypeFilter} /> diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx index 374b64b5674040..f352eef42794c0 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx @@ -5,8 +5,6 @@ * 2.0. */ -import React, { useState } from 'react'; -import { useQueryClient } from '@tanstack/react-query'; import { EuiButtonIcon, EuiContextMenuItem, @@ -19,27 +17,29 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { useQueryClient } from '@tanstack/react-query'; +import React, { useState } from 'react'; +import { rulesLocatorID, sloFeatureId } from '../../../../common'; +import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants'; import { sloKeys } from '../../../hooks/slo/query_key_factory'; import { useCapabilities } from '../../../hooks/slo/use_capabilities'; -import { useKibana } from '../../../utils/kibana_react'; import { useCloneSlo } from '../../../hooks/slo/use_clone_slo'; +import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; +import type { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; +import type { SloRule } from '../../../hooks/slo/use_fetch_rules_for_slo'; import { useGetFilteredRuleTypes } from '../../../hooks/use_get_filtered_rule_types'; -import { SloSummary } from './slo_summary'; -import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal'; -import { SloBadges } from './badges/slo_badges'; +import type { RulesParams } from '../../../locators/rules'; +import { paths } from '../../../routes/paths'; +import { useKibana } from '../../../utils/kibana_react'; import { - transformSloResponseToCreateSloForm, transformCreateSLOFormToCreateSLOInput, + transformSloResponseToCreateSloForm, } from '../../slo_edit/helpers/process_slo_form_values'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants'; -import { rulesLocatorID, sloFeatureId } from '../../../../common'; -import { paths } from '../../../routes/paths'; -import type { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; -import type { SloRule } from '../../../hooks/slo/use_fetch_rules_for_slo'; -import type { RulesParams } from '../../../locators/rules'; +import { SloBadges } from './badges/slo_badges'; +import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal'; +import { SloSummary } from './slo_summary'; export interface SloListItemProps { slo: SLOWithSummaryResponse; @@ -47,7 +47,6 @@ export interface SloListItemProps { historicalSummary?: HistoricalSummaryResponse[]; historicalSummaryLoading: boolean; activeAlerts?: ActiveAlerts; - onConfirmDelete: (slo: SLOWithSummaryResponse) => void; } export function SloListItem({ @@ -56,7 +55,6 @@ export function SloListItem({ historicalSummary = [], historicalSummaryLoading, activeAlerts, - onConfirmDelete, }: SloListItemProps) { const { application: { navigateToUrl }, @@ -72,6 +70,7 @@ export function SloListItem({ const filteredRuleTypes = useGetFilteredRuleTypes(); const { mutate: cloneSlo } = useCloneSlo(); + const { mutate: deleteSlo } = useDeleteSlo(); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false); @@ -100,15 +99,7 @@ export function SloListItem({ const handleNavigateToRules = async () => { const locator = locators.get(rulesLocatorID); - - locator?.navigate( - { - params: { sloId: slo.id }, - }, - { - replace: false, - } - ); + locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); }; const handleClone = () => { @@ -127,7 +118,7 @@ export function SloListItem({ const handleDeleteConfirm = () => { setDeleteConfirmationModalOpen(false); - onConfirmDelete(slo); + deleteSlo({ id: slo.id, name: slo.name }); }; const handleDeleteCancel = () => { diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx index 2eb790e404b30e..32828685faf620 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx @@ -4,17 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - import type { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import React from 'react'; import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; -import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo'; import { useFetchHistoricalSummary } from '../../../hooks/slo/use_fetch_historical_summary'; -import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; -import { SloListItem } from './slo_list_item'; +import { useFetchRulesForSlo } from '../../../hooks/slo/use_fetch_rules_for_slo'; import { SloListEmpty } from './slo_list_empty'; import { SloListError } from './slo_list_error'; +import { SloListItem } from './slo_list_item'; export interface Props { sloList: SLOWithSummaryResponse[]; @@ -30,8 +28,6 @@ export function SloListItems({ sloList, loading, error }: Props) { const { isLoading: historicalSummaryLoading, data: historicalSummaryBySlo } = useFetchHistoricalSummary({ sloIds }); - const { mutate: deleteSlo } = useDeleteSlo(); - if (!loading && !error && sloList.length === 0) { return ; } @@ -39,10 +35,6 @@ export function SloListItems({ sloList, loading, error }: Props) { return ; } - const handleDelete = (slo: SLOWithSummaryResponse) => { - deleteSlo({ id: slo.id, name: slo.name }); - }; - return ( {sloList.map((slo) => ( @@ -53,7 +45,6 @@ export function SloListItems({ sloList, loading, error }: Props) { historicalSummary={historicalSummaryBySlo?.[slo.id]} historicalSummaryLoading={historicalSummaryLoading} slo={slo} - onConfirmDelete={handleDelete} /> ))} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx index b5d7e26fe416bf..4b1c31209de3bc 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx @@ -28,7 +28,6 @@ const defaultProps: SloListSearchFilterSortBarProps = { loading: false, onChangeQuery: () => {}, onChangeSort: () => {}, - onChangeIndicatorTypeFilter: () => {}, }; export const SloListSearchFilterSortBar = Template.bind({}); diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx index 89bc65a8da8e5c..8a9b22c6b5c7fc 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx @@ -6,7 +6,6 @@ */ import { - EuiFieldSearch, EuiFilterButton, EuiFilterGroup, EuiFlexGroup, @@ -18,29 +17,18 @@ import { } from '@elastic/eui'; import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useState } from 'react'; -import { - INDICATOR_APM_AVAILABILITY, - INDICATOR_APM_LATENCY, - INDICATOR_CUSTOM_KQL, - INDICATOR_CUSTOM_METRIC, - INDICATOR_HISTOGRAM, -} from '../../../utils/slo/labels'; +import { QueryStringInput } from '@kbn/unified-search-plugin/public'; +import React, { useState } from 'react'; +import { useCreateDataView } from '../../../hooks/use_create_data_view'; +import { useKibana } from '../../../utils/kibana_react'; export interface SloListSearchFilterSortBarProps { loading: boolean; - onChangeQuery: (e: React.ChangeEvent) => void; - onChangeSort: (sort: SortType) => void; - onChangeIndicatorTypeFilter: (filter: FilterType[]) => void; + onChangeQuery: (query: string) => void; + onChangeSort: (sort: SortField | undefined) => void; } -export type SortType = 'creationTime' | 'indicatorType'; -export type FilterType = - | 'sli.apm.transactionDuration' - | 'sli.apm.transactionErrorRate' - | 'sli.kql.custom' - | 'sli.metric.custom' - | 'sli.histogram.custom'; +export type SortField = 'sli_value' | 'error_budget_consumed' | 'error_budget_remaining' | 'status'; export type Item = EuiSelectableOption & { label: string; @@ -48,42 +36,31 @@ export type Item = EuiSelectableOption & { checked?: EuiSelectableOptionCheckedType; }; -const SORT_OPTIONS: Array> = [ +const SORT_OPTIONS: Array> = [ { - label: i18n.translate('xpack.observability.slo.list.sortBy.creationTime', { - defaultMessage: 'Creation time', + label: i18n.translate('xpack.observability.slo.list.sortBy.sliValue', { + defaultMessage: 'SLI value', }), - type: 'creationTime', - checked: 'on', + type: 'sli_value', }, { - label: i18n.translate('xpack.observability.slo.list.sortBy.indicatorType', { - defaultMessage: 'Indicator type', + label: i18n.translate('xpack.observability.slo.list.sortBy.sloStatus', { + defaultMessage: 'SLO status', }), - type: 'indicatorType', - }, -]; - -const INDICATOR_TYPE_OPTIONS: Array> = [ - { - label: INDICATOR_APM_LATENCY, - type: 'sli.apm.transactionDuration', - }, - { - label: INDICATOR_APM_AVAILABILITY, - type: 'sli.apm.transactionErrorRate', - }, - { - label: INDICATOR_CUSTOM_KQL, - type: 'sli.kql.custom', + type: 'status', + checked: 'on', }, { - label: INDICATOR_CUSTOM_METRIC, - type: 'sli.metric.custom', + label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', { + defaultMessage: 'Error budget consumed', + }), + type: 'error_budget_consumed', }, { - label: INDICATOR_HISTOGRAM, - type: 'sli.histogram.custom', + label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetRemaining', { + defaultMessage: 'Error budget remaining', + }), + type: 'error_budget_remaining', }, ]; @@ -91,96 +68,60 @@ export function SloListSearchFilterSortBar({ loading, onChangeQuery, onChangeSort, - onChangeIndicatorTypeFilter, }: SloListSearchFilterSortBarProps) { - const [isFilterPopoverOpen, setFilterPopoverOpen] = useState(false); - const [isSortPopoverOpen, setSortPopoverOpen] = useState(false); + const { data, dataViews, docLinks, http, notifications, storage, uiSettings, unifiedSearch } = + useKibana().services; + const { dataView } = useCreateDataView({ indexPatternString: '.slo-observability.summary-*' }); + const [isSortPopoverOpen, setSortPopoverOpen] = useState(false); const [sortOptions, setSortOptions] = useState(SORT_OPTIONS); - const [indicatorTypeOptions, setIndicatorTypeOptions] = useState(INDICATOR_TYPE_OPTIONS); + const [query, setQuery] = useState(''); const selectedSort = sortOptions.find((option) => option.checked === 'on'); - const selectedIndicatorTypeFilter = indicatorTypeOptions.filter( - (option) => option.checked === 'on' - ); - - const handleToggleFilterButton = () => setFilterPopoverOpen(!isFilterPopoverOpen); const handleToggleSortButton = () => setSortPopoverOpen(!isSortPopoverOpen); - const handleChangeSort = (newOptions: Array>) => { + const handleChangeSort = (newOptions: Array>) => { setSortOptions(newOptions); setSortPopoverOpen(false); + onChangeSort(newOptions.find((o) => o.checked)?.type); }; - const handleChangeIndicatorTypeOptions = (newOptions: Array>) => { - setIndicatorTypeOptions(newOptions); - onChangeIndicatorTypeFilter( - newOptions.filter((option) => option.checked === 'on').map((option) => option.type) - ); - }; - - useEffect(() => { - if (selectedSort?.type === 'creationTime' || selectedSort?.type === 'indicatorType') { - onChangeSort(selectedSort.type); - } - }, [onChangeSort, selectedSort]); - return ( - onChangeQuery(query)} + disableLanguageSwitcher + isDisabled={loading} + indexPatterns={dataView ? [dataView] : []} placeholder={i18n.translate('xpack.observability.slo.list.search', { - defaultMessage: 'Search', + defaultMessage: 'Search your SLOs...', })} + query={{ query: String(query), language: 'kuery' }} + size="s" + onChange={(value) => setQuery(String(value.query))} /> - - - - {i18n.translate('xpack.observability.slo.list.indicatorTypeFilter', { - defaultMessage: 'Indicator type', - })} - - } - isOpen={isFilterPopoverOpen} - closePopover={handleToggleFilterButton} - panelPaddingSize="none" - anchorPosition="downCenter" - > -
- - {i18n.translate('xpack.observability.slo.list.indicatorTypeFilter', { - defaultMessage: 'Indicator type', - })} - - > - options={indicatorTypeOptions} - onChange={handleChangeIndicatorTypeOptions} - > - {(list) => list} - -
-
-
-
- - + - > + > singleSelection options={sortOptions} onChange={handleChangeSort} + isLoading={loading} > {(list) => list} diff --git a/x-pack/plugins/observability/public/pages/slos/slos.test.tsx b/x-pack/plugins/observability/public/pages/slos/slos.test.tsx index 0435a66439222e..7f85774f69300d 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.test.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.test.tsx @@ -69,6 +69,20 @@ const mockKibana = () => { services: { application: { navigateToUrl: mockNavigate }, charts: chartPluginMock.createSetupContract(), + data: { + dataViews: { + find: jest.fn().mockReturnValue([]), + get: jest.fn().mockReturnValue([]), + }, + }, + dataViews: { + create: jest.fn().mockResolvedValue(42), + }, + docLinks: { + links: { + query: {}, + }, + }, http: { basePath: { prepend: (url: string) => url, @@ -87,6 +101,9 @@ const mockKibana = () => { }, }, }, + storage: { + get: () => {}, + }, triggersActionsUi: { getAddRuleFlyout: mockGetAddRuleFlyout }, uiSettings: { get: (settings: string) => { @@ -95,6 +112,11 @@ const mockKibana = () => { return ''; }, }, + unifiedSearch: { + autocomplete: { + hasQuerySuggestions: () => {}, + }, + }, }, }); }; diff --git a/x-pack/plugins/observability/public/pages/slos/slos.tsx b/x-pack/plugins/observability/public/pages/slos/slos.tsx index c2a07a01e0fa15..613e10b862abf3 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.tsx @@ -31,8 +31,7 @@ export function SlosPage() { const { hasAtLeast } = useLicense(); const { isInitialLoading, isLoading, isError, sloList } = useFetchSloList(); - - const { total } = sloList || {}; + const { total } = sloList || { total: 0 }; const [isAutoRefreshing, setIsAutoRefreshing] = useState(true); diff --git a/x-pack/plugins/observability/public/typings/slo/index.ts b/x-pack/plugins/observability/public/typings/slo/index.ts index 3b02b76ecacc29..1bc3d7f8de69db 100644 --- a/x-pack/plugins/observability/public/typings/slo/index.ts +++ b/x-pack/plugins/observability/public/typings/slo/index.ts @@ -7,7 +7,7 @@ import { RuleTypeParams } from '@kbn/alerting-plugin/common'; -type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M' | 'Y'; +type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M'; interface Duration { value: number; diff --git a/x-pack/plugins/observability/public/utils/slo/duration.ts b/x-pack/plugins/observability/public/utils/slo/duration.ts index 2be1a51a69eab8..4e643261169795 100644 --- a/x-pack/plugins/observability/public/utils/slo/duration.ts +++ b/x-pack/plugins/observability/public/utils/slo/duration.ts @@ -28,24 +28,17 @@ export function toMinutes(duration: Duration) { return duration.value * 7 * 24 * 60; case 'M': return duration.value * 30 * 24 * 60; - case 'Y': - return duration.value * 365 * 24 * 60; + default: + assertNever(duration.unit); } - - assertNever(duration.unit); } -export function toMomentUnitOfTime(unit: string): moment.unitOfTime.Diff | undefined { +export function toCalendarAlignedMomentUnitOfTime(unit: string): moment.unitOfTime.StartOf { switch (unit) { - case 'd': - return 'days'; + default: case 'w': - return 'weeks'; + return 'isoWeek'; case 'M': return 'months'; - case 'Q': - return 'quarters'; - case 'Y': - return 'years'; } } diff --git a/x-pack/plugins/observability/public/utils/slo/labels.ts b/x-pack/plugins/observability/public/utils/slo/labels.ts index 43384539f2a849..40c58e624bb2b9 100644 --- a/x-pack/plugins/observability/public/utils/slo/labels.ts +++ b/x-pack/plugins/observability/public/utils/slo/labels.ts @@ -112,13 +112,6 @@ export function toDurationLabel(durationStr: string): string { duration: duration.value, }, }); - case 'Y': - return i18n.translate('xpack.observability.slo.duration.year', { - defaultMessage: '{duration, plural, one {1 year} other {# years}}', - values: { - duration: duration.value, - }, - }); } } @@ -146,9 +139,5 @@ export function toDurationAdverbLabel(durationStr: string): string { return i18n.translate('xpack.observability.slo.duration.monthly', { defaultMessage: 'Monthly', }); - case 'Y': - return i18n.translate('xpack.observability.slo.duration.yearly', { - defaultMessage: 'Yearly', - }); } } 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 6dec5baf58faf3..f790a1bfe0a543 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 @@ -5,6 +5,8 @@ * 2.0. */ +import { SLO_RESOURCES_VERSION } from '../constants'; + export const getSLOMappingsTemplate = (name: string) => ({ name, template: { @@ -14,6 +16,31 @@ export const getSLOMappingsTemplate = (name: string) => ({ type: 'date', format: 'date_optional_time||epoch_millis', }, + // APM service and transaction specific fields + service: { + properties: { + name: { + type: 'keyword', + ignore_above: 256, + }, + environment: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + transaction: { + properties: { + name: { + type: 'keyword', + ignore_above: 256, + }, + type: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, slo: { properties: { id: { @@ -23,6 +50,53 @@ export const getSLOMappingsTemplate = (name: string) => ({ revision: { type: 'long', }, + 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', }, @@ -32,9 +106,6 @@ export const getSLOMappingsTemplate = (name: string) => ({ isGoodSlice: { type: 'byte', }, - context: { - type: 'flattened', - }, }, }, }, @@ -42,7 +113,7 @@ export const getSLOMappingsTemplate = (name: string) => ({ }, _meta: { description: 'Mappings for SLO rollup data', - version: 1, + version: SLO_RESOURCES_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts b/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts index f4e4c650031797..8832c51e030be0 100644 --- a/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts +++ b/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { SLO_RESOURCES_VERSION } from '../constants'; + export const getSLOSettingsTemplate = (name: string) => ({ name, template: { @@ -14,7 +16,7 @@ export const getSLOSettingsTemplate = (name: string) => ({ }, _meta: { description: 'Settings for SLO rollup data', - version: 1, + version: SLO_RESOURCES_VERSION, managed: true, managed_by: 'observability', }, 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 new file mode 100644 index 00000000000000..4a3a53a4725ef1 --- /dev/null +++ b/x-pack/plugins/observability/server/assets/component_templates/slo_summary_mappings_template.ts @@ -0,0 +1,128 @@ +/* + * 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 { SLO_RESOURCES_VERSION } from '../constants'; + +export const getSLOSummaryMappingsTemplate = (name: string) => ({ + name, + template: { + mappings: { + properties: { + // APM service and transaction specific fields + service: { + properties: { + name: { + type: 'keyword', + ignore_above: 256, + }, + environment: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + transaction: { + properties: { + name: { + type: 'keyword', + ignore_above: 256, + }, + type: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + slo: { + properties: { + id: { + type: 'keyword', + ignore_above: 256, + }, + revision: { + type: 'long', + }, + 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, + }, + }, + }, + budgetingMethod: { + type: 'keyword', + }, + timeWindow: { + properties: { + duration: { + type: 'keyword', + }, + type: { + type: 'keyword', + }, + }, + }, + }, + }, + sliValue: { + type: 'double', + }, + goodEvents: { + type: 'long', + }, + totalEvents: { + type: 'long', + }, + errorBudgetInitial: { + type: 'double', + }, + errorBudgetConsumed: { + type: 'double', + }, + errorBudgetRemaining: { + type: 'double', + }, + errorBudgetEstimated: { + type: 'boolean', + }, + statusCode: { + type: 'byte', + }, + status: { + type: 'keyword', + ignore_above: 32, + }, + isTempDoc: { + type: 'boolean', + }, + }, + }, + }, + _meta: { + description: 'SLO summary mappings template', + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, +}); diff --git a/x-pack/plugins/observability/server/assets/component_templates/slo_summary_settings_template.ts b/x-pack/plugins/observability/server/assets/component_templates/slo_summary_settings_template.ts new file mode 100644 index 00000000000000..9224e532549dd8 --- /dev/null +++ b/x-pack/plugins/observability/server/assets/component_templates/slo_summary_settings_template.ts @@ -0,0 +1,23 @@ +/* + * 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 { SLO_RESOURCES_VERSION } from '../constants'; + +export const getSLOSummarySettingsTemplate = (name: string) => ({ + name, + template: { + settings: { + hidden: true, + }, + }, + _meta: { + description: 'SLO summary settings template', + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, +}); diff --git a/x-pack/plugins/observability/server/assets/constants.ts b/x-pack/plugins/observability/server/assets/constants.ts index 76543a27b62660..04161ba9303106 100644 --- a/x-pack/plugins/observability/server/assets/constants.ts +++ b/x-pack/plugins/observability/server/assets/constants.ts @@ -5,13 +5,32 @@ * 2.0. */ +export const SLO_RESOURCES_VERSION = 2; + export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.sli-mappings'; export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.sli-settings'; + export const SLO_INDEX_TEMPLATE_NAME = '.slo-observability.sli'; -export const SLO_RESOURCES_VERSION = 1; -export const SLO_INGEST_PIPELINE_NAME = `${SLO_INDEX_TEMPLATE_NAME}.monthly`; -export const SLO_DESTINATION_INDEX_NAME = `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}`; -export const SLO_DESTINATION_INDEX_PATTERN = `${SLO_DESTINATION_INDEX_NAME}*`; +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_INDEX_NAME_PREFIX = `.slo-observability.sli-v${SLO_RESOURCES_VERSION}.`; + +export const SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME = '.slo-observability.summary-mappings'; +export const SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME = '.slo-observability.summary-settings'; +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}`; diff --git a/x-pack/plugins/observability/server/assets/index_templates/slo_index_templates.ts b/x-pack/plugins/observability/server/assets/index_templates/slo_index_templates.ts index adc6c47a1e2599..fc9ed97f95cf8c 100644 --- a/x-pack/plugins/observability/server/assets/index_templates/slo_index_templates.ts +++ b/x-pack/plugins/observability/server/assets/index_templates/slo_index_templates.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { SLO_RESOURCES_VERSION } from '../constants'; + export const getSLOIndexTemplate = (name: string, indexPattern: string, composedOf: string[]) => ({ name, index_patterns: [indexPattern], @@ -12,7 +14,7 @@ export const getSLOIndexTemplate = (name: string, indexPattern: string, composed priority: 500, _meta: { description: 'Template for SLO rollup data', - version: 1, + version: SLO_RESOURCES_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/assets/index_templates/slo_summary_index_templates.ts b/x-pack/plugins/observability/server/assets/index_templates/slo_summary_index_templates.ts new file mode 100644 index 00000000000000..a32c0d84169211 --- /dev/null +++ b/x-pack/plugins/observability/server/assets/index_templates/slo_summary_index_templates.ts @@ -0,0 +1,25 @@ +/* + * 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 { SLO_RESOURCES_VERSION } from '../constants'; + +export const getSLOSummaryIndexTemplate = ( + name: string, + indexPattern: string, + composedOf: string[] +) => ({ + name, + index_patterns: [indexPattern], + composed_of: composedOf, + priority: 500, + _meta: { + description: 'SLO summary index template', + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, +}); 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 c33e3400d60cdd..eafa7399a7c359 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 @@ -5,6 +5,8 @@ * 2.0. */ +import { SLO_RESOURCES_VERSION } from '../constants'; + export const getSLOPipelineTemplate = (id: string, indexNamePrefix: string) => ({ id, description: 'Monthly date-time index naming for SLO data', @@ -19,7 +21,7 @@ export const getSLOPipelineTemplate = (id: string, indexNamePrefix: string) => ( ], _meta: { description: 'SLO ingest pipeline', - version: 1, + 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 new file mode 100644 index 00000000000000..d3c8e04b93730f --- /dev/null +++ b/x-pack/plugins/observability/server/assets/ingest_templates/slo_summary_pipeline_template.ts @@ -0,0 +1,60 @@ +/* + * 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 { SLO_RESOURCES_VERSION } from '../constants'; + +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: ',', + }, + }, + { + 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', + }, + }, + ], + _meta: { + description: 'SLO summary ingest pipeline', + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, +}); diff --git a/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts index daa049033ec476..8869780a969ce5 100644 --- a/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts +++ b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts @@ -12,6 +12,7 @@ import { TransformSource, TransformTimeSync, } from '@elastic/elasticsearch/lib/api/types'; +import { SLO_RESOURCES_VERSION } from '../constants'; export interface TransformSettings { frequency: TransformPutTransformRequest['frequency']; @@ -47,7 +48,7 @@ export const getSLOTransformTemplate = ( aggregations, }, _meta: { - version: 1, + version: SLO_RESOURCES_VERSION, managed: true, managed_by: 'observability', }, diff --git a/x-pack/plugins/observability/server/domain/models/time_window.ts b/x-pack/plugins/observability/server/domain/models/time_window.ts index 30aa35cbd9f721..aa12aa70a8ae8b 100644 --- a/x-pack/plugins/observability/server/domain/models/time_window.ts +++ b/x-pack/plugins/observability/server/domain/models/time_window.ts @@ -5,10 +5,42 @@ * 2.0. */ +import { + calendarAlignedTimeWindowSchema, + rollingTimeWindowSchema, + timeWindowSchema, +} from '@kbn/slo-schema'; +import moment from 'moment'; import * as t from 'io-ts'; -import { rollingTimeWindowSchema, timeWindowSchema } from '@kbn/slo-schema'; type TimeWindow = t.TypeOf; type RollingTimeWindow = t.TypeOf; +type CalendarAlignedTimeWindow = t.TypeOf; -export type { RollingTimeWindow, TimeWindow }; +export type { RollingTimeWindow, TimeWindow, CalendarAlignedTimeWindow }; + +export function toCalendarAlignedTimeWindowMomentUnit( + timeWindow: CalendarAlignedTimeWindow +): moment.unitOfTime.StartOf { + const unit = timeWindow.duration.unit; + switch (unit) { + case 'w': + return 'isoWeeks'; + case 'M': + return 'months'; + default: + throw new Error(`Invalid calendar aligned time window duration unit: ${unit}`); + } +} + +export function toRollingTimeWindowMomentUnit( + timeWindow: RollingTimeWindow +): moment.unitOfTime.Diff { + const unit = timeWindow.duration.unit; + switch (unit) { + case 'd': + return 'days'; + default: + throw new Error(`Invalid rolling time window duration unit: ${unit}`); + } +} diff --git a/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts b/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts index 542766b6c65442..84fef850e096c1 100644 --- a/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts +++ b/x-pack/plugins/observability/server/domain/services/compute_burn_rate.test.ts @@ -8,7 +8,7 @@ import { computeBurnRate } from './compute_burn_rate'; import { toDateRange } from './date_range'; import { createSLO } from '../../services/slo/fixtures/slo'; -import { sixHoursRolling } from '../../services/slo/fixtures/time_window'; +import { ninetyDaysRolling } from '../../services/slo/fixtures/time_window'; describe('computeBurnRate', () => { it('computes 0 when total is 0', () => { @@ -16,7 +16,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO(), { good: 10, total: 0, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(0); }); @@ -26,7 +26,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO(), { good: 9999, total: 1, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(0); }); @@ -36,7 +36,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO({ objective: { target: 0.9 } }), { good: 90, total: 100, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(1); }); @@ -46,7 +46,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO({ objective: { target: 0.99 } }), { good: 90, total: 100, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(10); }); @@ -56,7 +56,7 @@ describe('computeBurnRate', () => { computeBurnRate(createSLO({ objective: { target: 0.8 } }), { good: 90, total: 100, - dateRange: toDateRange(sixHoursRolling()), + dateRange: toDateRange(ninetyDaysRolling()), }) ).toEqual(0.5); }); diff --git a/x-pack/plugins/observability/server/domain/services/date_range.test.ts b/x-pack/plugins/observability/server/domain/services/date_range.test.ts index ea04358e708c53..524aecbfab10f5 100644 --- a/x-pack/plugins/observability/server/domain/services/date_range.test.ts +++ b/x-pack/plugins/observability/server/domain/services/date_range.test.ts @@ -5,25 +5,29 @@ * 2.0. */ -import { TimeWindow } from '../models/time_window'; -import { Duration } from '../models'; +import { + monthlyCalendarAligned, + ninetyDaysRolling, + sevenDaysRolling, + thirtyDaysRolling, + weeklyCalendarAligned, +} from '../../services/slo/fixtures/time_window'; import { toDateRange } from './date_range'; -import { oneMonth, oneQuarter, oneWeek, thirtyDays } from '../../services/slo/fixtures/duration'; const NOW = new Date('2022-08-11T08:31:00.000Z'); describe('toDateRange', () => { describe('for calendar aligned time window', () => { it('computes the date range for weekly calendar', () => { - const timeWindow = aCalendarTimeWindow(oneWeek()); + const timeWindow = weeklyCalendarAligned(); expect(toDateRange(timeWindow, NOW)).toEqual({ - from: new Date('2022-08-07T00:00:00.000Z'), - to: new Date('2022-08-13T23:59:59.999Z'), + from: new Date('2022-08-08T00:00:00.000Z'), + to: new Date('2022-08-14T23:59:59.999Z'), }); }); it('computes the date range for monthly calendar', () => { - const timeWindow = aCalendarTimeWindow(oneMonth()); + const timeWindow = monthlyCalendarAligned(); expect(toDateRange(timeWindow, NOW)).toEqual({ from: new Date('2022-08-01T00:00:00.000Z'), to: new Date('2022-08-31T23:59:59.999Z'), @@ -33,42 +37,24 @@ describe('toDateRange', () => { describe('for rolling time window', () => { it("computes the date range using a '30days' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(thirtyDays()), NOW)).toEqual({ + expect(toDateRange(thirtyDaysRolling(), NOW)).toEqual({ from: new Date('2022-07-12T08:31:00.000Z'), to: new Date('2022-08-11T08:31:00.000Z'), }); }); - it("computes the date range using a 'weekly' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(oneWeek()), NOW)).toEqual({ + it("computes the date range using a '7days' rolling window", () => { + expect(toDateRange(sevenDaysRolling(), NOW)).toEqual({ from: new Date('2022-08-04T08:31:00.000Z'), to: new Date('2022-08-11T08:31:00.000Z'), }); }); - it("computes the date range using a 'monthly' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(oneMonth()), NOW)).toEqual({ - from: new Date('2022-07-11T08:31:00.000Z'), - to: new Date('2022-08-11T08:31:00.000Z'), - }); - }); - - it("computes the date range using a 'quarterly' rolling window", () => { - expect(toDateRange(aRollingTimeWindow(oneQuarter()), NOW)).toEqual({ - from: new Date('2022-05-11T08:31:00.000Z'), + it("computes the date range using a '90days' rolling window", () => { + expect(toDateRange(ninetyDaysRolling(), NOW)).toEqual({ + from: new Date('2022-05-13T08:31:00.000Z'), to: new Date('2022-08-11T08:31:00.000Z'), }); }); }); }); - -function aCalendarTimeWindow(duration: Duration): TimeWindow { - return { - duration, - type: 'calendarAligned', - }; -} - -function aRollingTimeWindow(duration: Duration): TimeWindow { - return { duration, type: 'rolling' }; -} diff --git a/x-pack/plugins/observability/server/domain/services/date_range.ts b/x-pack/plugins/observability/server/domain/services/date_range.ts index 2d61498b4e8017..9c54197aa39e31 100644 --- a/x-pack/plugins/observability/server/domain/services/date_range.ts +++ b/x-pack/plugins/observability/server/domain/services/date_range.ts @@ -5,16 +5,19 @@ * 2.0. */ +import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; import moment from 'moment'; -import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema'; - -import { DateRange, toMomentUnitOfTime } from '../models'; -import type { TimeWindow } from '../models/time_window'; +import { DateRange } from '../models'; +import { + TimeWindow, + toCalendarAlignedTimeWindowMomentUnit, + toRollingTimeWindowMomentUnit, +} from '../models/time_window'; export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date()): DateRange => { if (calendarAlignedTimeWindowSchema.is(timeWindow)) { - const unit = toMomentUnitOfTime(timeWindow.duration.unit); + const unit = toCalendarAlignedTimeWindowMomentUnit(timeWindow); const from = moment.utc(currentDate).startOf(unit); const to = moment.utc(currentDate).endOf(unit); @@ -22,12 +25,14 @@ export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date } if (rollingTimeWindowSchema.is(timeWindow)) { - const unit = toMomentUnitOfTime(timeWindow.duration.unit); + const unit = toRollingTimeWindowMomentUnit(timeWindow); const now = moment.utc(currentDate).startOf('minute'); + const from = now.clone().subtract(timeWindow.duration.value, unit); + const to = now.clone(); return { - from: now.clone().subtract(timeWindow.duration.value, unit).toDate(), - to: now.toDate(), + from: from.toDate(), + to: to.toDate(), }; } diff --git a/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts b/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts index 12d3ebe77b82b9..70cc408ceb175f 100644 --- a/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts +++ b/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts @@ -8,6 +8,7 @@ import { validateSLO } from '.'; import { oneMinute, sixHours } from '../../services/slo/fixtures/duration'; import { createSLO } from '../../services/slo/fixtures/slo'; +import { sevenDaysRolling } from '../../services/slo/fixtures/time_window'; import { Duration, DurationUnit } from '../models'; describe('validateSLO', () => { @@ -41,16 +42,12 @@ describe('validateSLO', () => { { duration: new Duration(2, DurationUnit.Hour), shouldThrow: true }, { duration: new Duration(1, DurationUnit.Day), shouldThrow: true }, { duration: new Duration(7, DurationUnit.Day), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Week), shouldThrow: false }, { duration: new Duration(2, DurationUnit.Week), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Month), shouldThrow: false }, { duration: new Duration(2, DurationUnit.Month), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Quarter), shouldThrow: true }, - { duration: new Duration(3, DurationUnit.Quarter), shouldThrow: true }, - { duration: new Duration(1, DurationUnit.Year), shouldThrow: true }, - { duration: new Duration(3, DurationUnit.Year), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Week), shouldThrow: false }, + { duration: new Duration(1, DurationUnit.Month), shouldThrow: false }, ])( - 'throws when time window calendar aligned is not 1 week or 1 month', + 'throws when calendar aligned time window is not 1 week or 1 month', ({ duration, shouldThrow }) => { if (shouldThrow) { expect(() => @@ -72,6 +69,34 @@ describe('validateSLO', () => { } ); + it.each([ + { duration: new Duration(7, DurationUnit.Day), shouldThrow: false }, + { duration: new Duration(30, DurationUnit.Day), shouldThrow: false }, + { duration: new Duration(90, DurationUnit.Day), shouldThrow: false }, + { duration: new Duration(1, DurationUnit.Hour), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Day), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Week), shouldThrow: true }, + { duration: new Duration(1, DurationUnit.Month), shouldThrow: true }, + ])('throws when rolling time window is not 7, 30 or 90days', ({ duration, shouldThrow }) => { + if (shouldThrow) { + expect(() => + validateSLO( + createSLO({ + timeWindow: { duration, type: 'rolling' }, + }) + ) + ).toThrowError('Invalid time_window.duration'); + } else { + expect(() => + validateSLO( + createSLO({ + timeWindow: { duration, type: 'rolling' }, + }) + ) + ).not.toThrowError(); + } + }); + describe('settings', () => { it("throws when frequency is longer or equal than '1h'", () => { const slo = createSLO({ @@ -173,25 +198,11 @@ describe('validateSLO', () => { objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Month) }, }) ).toThrowError('Invalid objective.timeslice_window'); - - expect(() => - validateSLO({ - ...slo, - objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Quarter) }, - }) - ).toThrowError('Invalid objective.timeslice_window'); - - expect(() => - validateSLO({ - ...slo, - objective: { ...slo.objective, timesliceWindow: new Duration(1, DurationUnit.Year) }, - }) - ).toThrowError('Invalid objective.timeslice_window'); }); it("throws when 'objective.timeslice_window' is longer than 'slo.time_window'", () => { const slo = createSLO({ - timeWindow: { duration: new Duration(1, DurationUnit.Week), type: 'rolling' }, + timeWindow: sevenDaysRolling(), budgetingMethod: 'timeslices', objective: { target: 0.95, diff --git a/x-pack/plugins/observability/server/domain/services/validate_slo.ts b/x-pack/plugins/observability/server/domain/services/validate_slo.ts index a1207b2c17f521..eb253f44cdf5ab 100644 --- a/x-pack/plugins/observability/server/domain/services/validate_slo.ts +++ b/x-pack/plugins/observability/server/domain/services/validate_slo.ts @@ -85,13 +85,8 @@ function isValidTargetNumber(value: number): boolean { } function isValidRollingTimeWindowDuration(duration: Duration): boolean { - return [ - DurationUnit.Day, - DurationUnit.Week, - DurationUnit.Month, - DurationUnit.Quarter, - DurationUnit.Year, - ].includes(duration.unit); + // 7, 30 or 90days accepted + return duration.unit === DurationUnit.Day && [7, 30, 90].includes(duration.value); } function isValidCalendarAlignedTimeWindowDuration(duration: Duration): boolean { diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 02cc2f33cc9f59..0b0b7207d69ec8 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -5,47 +5,53 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; +import { PluginSetupContract, PluginStartContract } from '@kbn/alerting-plugin/server'; +import { + createUICapabilities as createCasesUICapabilities, + getApiTags as getCasesApiTags, +} from '@kbn/cases-plugin/common'; +import { CloudSetup } from '@kbn/cloud-plugin/server'; import { - PluginInitializerContext, - Plugin, CoreSetup, DEFAULT_APP_CATEGORIES, Logger, + Plugin, + PluginInitializerContext, } from '@kbn/core/server'; +import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server'; import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; -import { PluginSetupContract, PluginStartContract } from '@kbn/alerting-plugin/server'; +import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; +import { i18n } from '@kbn/i18n'; import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; -import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server'; -import { - createUICapabilities as createCasesUICapabilities, - getApiTags as getCasesApiTags, -} from '@kbn/cases-plugin/common'; import { SharePluginSetup } from '@kbn/share-plugin/server'; import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; -import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; -import { CloudSetup } from '@kbn/cloud-plugin/server'; +import { ObservabilityConfig } from '.'; +import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common'; +import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants'; import { - kubernetesGuideId, kubernetesGuideConfig, + kubernetesGuideId, } from '../common/guided_onboarding/kubernetes_guide_config'; -import { ObservabilityConfig } from '.'; +import { AlertsLocatorDefinition } from '../common/locators/alerts'; import { + AnnotationsAPI, bootstrapAnnotations, ScopedAnnotationsClientFactory, - AnnotationsAPI, } from './lib/annotations/bootstrap_annotations'; -import { uiSettings } from './ui_settings'; -import { registerRoutes } from './routes/register_routes'; +import { registerSloUsageCollector } from './lib/collectors/register'; +import { registerRuleTypes } from './lib/rules/register_rule_types'; import { getObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; +import { registerRoutes } from './routes/register_routes'; import { compositeSlo, slo, SO_COMPOSITE_SLO_TYPE, SO_SLO_TYPE } from './saved_objects'; -import { AlertsLocatorDefinition } from '../common/locators/alerts'; -import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common'; -import { registerRuleTypes } from './lib/rules/register_rule_types'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants'; -import { registerSloUsageCollector } from './lib/collectors/register'; import { threshold } from './saved_objects/threshold'; +import { + DefaultResourceInstaller, + DefaultSLOInstaller, + DefaultSummaryTransformInstaller, +} from './services/slo'; + +import { uiSettings } from './ui_settings'; export type ObservabilityPluginSetup = ReturnType; @@ -258,6 +264,20 @@ export class ObservabilityPlugin implements Plugin { logger: this.logger, repository: getObservabilityServerRouteRepository(config), }); + + 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 + ); + sloInstaller.install(); }); /** diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index c98a1579d20420..3ff501c52bd004 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -5,22 +5,22 @@ * 2.0. */ -import { forbidden, failedDependency } from '@hapi/boom'; +import { failedDependency, forbidden } from '@hapi/boom'; import { createSLOParamsSchema, deleteSLOParamsSchema, fetchHistoricalSummaryParamsSchema, findSLOParamsSchema, - getSLOBurnRatesParamsSchema, getPreviewDataParamsSchema, + getSLOBurnRatesParamsSchema, getSLODiagnosisParamsSchema, getSLOParamsSchema, manageSLOParamsSchema, updateSLOParamsSchema, } from '@kbn/slo-schema'; +import type { IndicatorTypes } from '../../domain/models'; import { CreateSLO, - DefaultResourceInstaller, DefaultSummaryClient, DefaultTransformManager, DeleteSLO, @@ -29,6 +29,13 @@ import { KibanaSavedObjectsSLORepository, UpdateSLO, } from '../../services/slo'; +import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summary'; +import { getBurnRates } from '../../services/slo/get_burn_rates'; +import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis'; +import { GetPreviewData } from '../../services/slo/get_preview_data'; +import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client'; +import { ManageSLO } from '../../services/slo/manage_slo'; +import { DefaultSummarySearchClient } from '../../services/slo/summary_search_client'; import { ApmTransactionDurationTransformGenerator, ApmTransactionErrorRateTransformGenerator, @@ -37,15 +44,8 @@ import { MetricCustomTransformGenerator, TransformGenerator, } from '../../services/slo/transform_generators'; -import { createObservabilityServerRoute } from '../create_observability_server_route'; -import { DefaultHistoricalSummaryClient } from '../../services/slo/historical_summary_client'; -import { FetchHistoricalSummary } from '../../services/slo/fetch_historical_summary'; -import type { IndicatorTypes } from '../../domain/models'; import type { ObservabilityRequestHandlerContext } from '../../types'; -import { ManageSLO } from '../../services/slo/manage_slo'; -import { getGlobalDiagnosis, getSloDiagnosis } from '../../services/slo/get_diagnosis'; -import { getBurnRates } from '../../services/slo/get_burn_rates'; -import { GetPreviewData } from '../../services/slo/get_preview_data'; +import { createObservabilityServerRoute } from '../create_observability_server_route'; const transformGenerators: Record = { 'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(), @@ -75,11 +75,9 @@ const createSLORoute = createObservabilityServerRoute({ const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; - - const resourceInstaller = new DefaultResourceInstaller(esClient, logger); const repository = new KibanaSavedObjectsSLORepository(soClient); const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); - const createSLO = new CreateSLO(resourceInstaller, repository, transformManager); + const createSLO = new CreateSLO(esClient, repository, transformManager); const response = await createSLO.execute(params.body); @@ -228,7 +226,7 @@ const findSLORoute = createObservabilityServerRoute({ tags: ['access:slo_read'], }, params: findSLOParamsSchema, - handler: async ({ context, params }) => { + handler: async ({ context, params, logger }) => { const hasCorrectLicense = await isLicenseAtLeastPlatinum(context); if (!hasCorrectLicense) { @@ -238,8 +236,8 @@ const findSLORoute = createObservabilityServerRoute({ const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const repository = new KibanaSavedObjectsSLORepository(soClient); - const summaryClient = new DefaultSummaryClient(esClient); - const findSLO = new FindSLO(repository, summaryClient); + const summarySearchClient = new DefaultSummarySearchClient(esClient, logger); + const findSLO = new FindSLO(repository, summarySearchClient); const response = await findSLO.execute(params?.query ?? {}); @@ -304,8 +302,9 @@ const getSloDiagnosisRoute = createObservabilityServerRoute({ handler: async ({ context, params }) => { const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; + const repository = new KibanaSavedObjectsSLORepository(soClient); - return getSloDiagnosis(params.path.id, { esClient, soClient }); + return getSloDiagnosis(params.path.id, { esClient, repository }); }, }); diff --git a/x-pack/plugins/observability/server/services/composite_slo/__snapshots__/summary_client.test.ts.snap b/x-pack/plugins/observability/server/services/composite_slo/__snapshots__/summary_client.test.ts.snap index 95f3a8c11fc658..ec4d29cba2f102 100644 --- a/x-pack/plugins/observability/server/services/composite_slo/__snapshots__/summary_client.test.ts.snap +++ b/x-pack/plugins/observability/server/services/composite_slo/__snapshots__/summary_client.test.ts.snap @@ -16,7 +16,7 @@ Object { exports[`SummaryClient fetchSummary with a rolling and occurrences composite SLO returns the summary 2`] = ` Array [ Object { - "index": ".slo-observability.sli-v1*", + "index": ".slo-observability.sli-v2*", }, Object { "aggs": Object { @@ -108,7 +108,7 @@ Object { exports[`SummaryClient with rolling and timeslices SLO returns the summary 2`] = ` Array [ Object { - "index": ".slo-observability.sli-v1*", + "index": ".slo-observability.sli-v2*", }, Object { "aggs": Object { 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 new file mode 100644 index 00000000000000..096820bc3ea8a0 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/create_slo.test.ts.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CreateSLO happy path calls the expected services 1`] = ` +Array [ + Object { + "document": Object { + "errorBudgetConsumed": 0, + "errorBudgetEstimated": false, + "errorBudgetInitial": 0.010000000000000009, + "errorBudgetRemaining": 1, + "goodEvents": 0, + "isTempDoc": true, + "service": Object { + "environment": null, + "name": null, + }, + "sliValue": -1, + "slo": Object { + "budgetingMethod": "occurrences", + "description": "irrelevant", + "id": "unique-id", + "indicator": Object { + "type": "sli.apm.transactionErrorRate", + }, + "instanceId": "*", + "name": "irrelevant", + "revision": 1, + "tags": Array [], + "timeWindow": Object { + "duration": "7d", + "type": "rolling", + }, + }, + "status": "NO_DATA", + "statusCode": 0, + "totalEvents": 0, + "transaction": Object { + "name": null, + "type": null, + }, + }, + "id": "slo-unique-id", + "index": ".slo-observability.summary-v2.temp", + }, +] +`; 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 new file mode 100644 index 00000000000000..4f40feb98d8405 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_search_client.test.ts.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 1`] = ` +Array [ + Object { + "index": ".slo-observability.summary-v2*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "slo.id": Array [ + "slo-one", + "slo_two", + "slo-three", + "slo-five", + ], + }, + }, + Object { + "term": Object { + "isTempDoc": true, + }, + }, + ], + }, + }, + "wait_for_completion": false, + }, +] +`; + +exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 2`] = ` +Object { + "page": 1, + "perPage": 20, + "results": Array [ + Object { + "id": "slo-one", + "summary": Object { + "errorBudget": Object { + "consumed": 0.4, + "initial": 0.02, + "isEstimated": false, + "remaining": 0.6, + }, + "sliValue": 0.9, + "status": "HEALTHY", + }, + }, + Object { + "id": "slo_two", + "summary": Object { + "errorBudget": Object { + "consumed": 0.4, + "initial": 0.02, + "isEstimated": false, + "remaining": 0.6, + }, + "sliValue": 0.9, + "status": "HEALTHY", + }, + }, + Object { + "id": "slo-three", + "summary": Object { + "errorBudget": Object { + "consumed": 0.4, + "initial": 0.02, + "isEstimated": false, + "remaining": 0.6, + }, + "sliValue": 0.9, + "status": "HEALTHY", + }, + }, + Object { + "id": "slo-five", + "summary": Object { + "errorBudget": Object { + "consumed": 0.4, + "initial": 0.02, + "isEstimated": false, + "remaining": 0.6, + }, + "sliValue": 0.9, + "status": "HEALTHY", + }, + }, + Object { + "id": "slo-four", + "summary": Object { + "errorBudget": Object { + "consumed": 0.4, + "initial": 0.02, + "isEstimated": false, + "remaining": 0.6, + }, + "sliValue": 0.9, + "status": "HEALTHY", + }, + }, + ], + "total": 5, +} +`; 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 new file mode 100644 index 00000000000000..fcdc584a825c0a --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/update_slo.test.ts.snap @@ -0,0 +1,49 @@ +// 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, + }, + "sliValue": -1, + "slo": Object { + "budgetingMethod": "occurrences", + "description": "irrelevant", + "id": "unique-id", + "indicator": Object { + "type": "sli.apm.transactionErrorRate", + }, + "instanceId": "*", + "name": "irrelevant", + "revision": 2, + "tags": Array [ + "critical", + "k8s", + ], + "timeWindow": Object { + "duration": "7d", + "type": "rolling", + }, + }, + "status": "NO_DATA", + "statusCode": 0, + "totalEvents": 0, + "transaction": Object { + "name": null, + "type": null, + }, + }, + "id": "slo-unique-id", + "index": ".slo-observability.summary-v2.temp", + }, +] +`; 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 6b44fc10df3d31..0e690027b93b63 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,43 +5,41 @@ * 2.0. */ +import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { CreateSLO } from './create_slo'; import { fiveMinute, oneMinute } from './fixtures/duration'; import { createAPMTransactionErrorRateIndicator, createSLOParams } from './fixtures/slo'; -import { - createResourceInstallerMock, - createSLORepositoryMock, - createTransformManagerMock, -} from './mocks'; -import { ResourceInstaller } from './resource_installer'; +import { createSLORepositoryMock, createTransformManagerMock } from './mocks'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; describe('CreateSLO', () => { - let mockResourceInstaller: jest.Mocked; + let esClientMock: ElasticsearchClientMock; let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; let createSLO: CreateSLO; beforeEach(() => { - mockResourceInstaller = createResourceInstallerMock(); + esClientMock = elasticsearchServiceMock.createElasticsearchClient(); mockRepository = createSLORepositoryMock(); mockTransformManager = createTransformManagerMock(); - createSLO = new CreateSLO(mockResourceInstaller, mockRepository, mockTransformManager); + createSLO = new CreateSLO(esClientMock, mockRepository, mockTransformManager); }); describe('happy path', () => { it('calls the expected services', async () => { - const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); + const sloParams = createSLOParams({ + id: 'unique-id', + indicator: createAPMTransactionErrorRateIndicator(), + }); mockTransformManager.install.mockResolvedValue('slo-transform-id'); const response = await createSLO.execute(sloParams); - expect(mockResourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalled(); expect(mockRepository.save).toHaveBeenCalledWith( expect.objectContaining({ ...sloParams, - id: expect.any(String), + id: 'unique-id', settings: { syncDelay: oneMinute(), frequency: oneMinute(), @@ -55,10 +53,11 @@ describe('CreateSLO', () => { { throwOnConflict: true } ); expect(mockTransformManager.install).toHaveBeenCalledWith( - expect.objectContaining({ ...sloParams, id: expect.any(String) }) + expect.objectContaining({ ...sloParams, id: 'unique-id' }) ); expect(mockTransformManager.start).toHaveBeenCalledWith('slo-transform-id'); - expect(response).toEqual(expect.objectContaining({ id: expect.any(String) })); + expect(response).toEqual(expect.objectContaining({ id: 'unique-id' })); + expect(esClientMock.index.mock.calls[0]).toMatchSnapshot(); }); it('overrides the default values when provided', async () => { @@ -73,7 +72,6 @@ describe('CreateSLO', () => { await createSLO.execute(sloParams); - expect(mockResourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalled(); expect(mockRepository.save).toHaveBeenCalledWith( expect.objectContaining({ ...sloParams, 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 7039e67329fc67..fe587c592dee89 100644 --- a/x-pack/plugins/observability/server/services/slo/create_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/create_slo.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { v1 as uuidv1 } from 'uuid'; - +import { ElasticsearchClient } from '@kbn/core/server'; import { CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema'; - +import { v1 as uuidv1 } from 'uuid'; +import { SLO_SUMMARY_TEMP_INDEX_NAME } from '../../assets/constants'; import { Duration, DurationUnit, SLO } from '../../domain/models'; -import { ResourceInstaller } from './resource_installer'; +import { validateSLO } from '../../domain/services'; import { SLORepository } from './slo_repository'; +import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; -import { validateSLO } from '../../domain/services'; export class CreateSLO { constructor( - private resourceInstaller: ResourceInstaller, + private esClient: ElasticsearchClient, private repository: SLORepository, private transformManager: TransformManager ) {} @@ -26,9 +26,7 @@ export class CreateSLO { const slo = this.toSLO(params); validateSLO(slo); - await this.resourceInstaller.ensureCommonResourcesInstalled(); await this.repository.save(slo, { throwOnConflict: true }); - let sloTransformId; try { sloTransformId = await this.transformManager.install(slo); @@ -48,6 +46,12 @@ export class CreateSLO { throw err; } + await this.esClient.index({ + index: SLO_SUMMARY_TEMP_INDEX_NAME, + id: `slo-${slo.id}`, + document: createTempSummaryDocument(slo), + }); + return this.toResponse(slo); } 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 e1e76fa56400d3..a136945edcd2c9 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 @@ -5,11 +5,15 @@ * 2.0. */ -import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; 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_INDEX_TEMPLATE_NAME } from '../../assets/constants'; +import { + getSLOTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, +} from '../../assets/constants'; import { DeleteSLO } from './delete_slo'; import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo'; import { createSLORepositoryMock, createTransformManagerMock } from './mocks'; @@ -45,9 +49,22 @@ describe('DeleteSLO', () => { expect(mockTransformManager.uninstall).toHaveBeenCalledWith( getSLOTransformId(slo.id, slo.revision) ); - expect(mockEsClient.deleteByQuery).toHaveBeenCalledWith( + 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_INDEX_TEMPLATE_NAME}*`, + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, query: { match: { 'slo.id': slo.id, 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 dc812b1bd4f31f..508da3b4ae129d 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.ts @@ -7,8 +7,11 @@ import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; import { ElasticsearchClient } from '@kbn/core/server'; -import { getSLOTransformId, SLO_INDEX_TEMPLATE_NAME } from '../../assets/constants'; - +import { + getSLOTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, +} from '../../assets/constants'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; @@ -28,13 +31,14 @@ export class DeleteSLO { await this.transformManager.uninstall(sloTransformId); await this.deleteRollupData(slo.id); + await this.deleteSummaryData(slo.id); await this.deleteAssociatedRules(slo.id); await this.repository.deleteById(slo.id); } private async deleteRollupData(sloId: string): Promise { await this.esClient.deleteByQuery({ - index: `${SLO_INDEX_TEMPLATE_NAME}*`, + index: SLO_DESTINATION_INDEX_PATTERN, wait_for_completion: false, query: { match: { @@ -44,6 +48,17 @@ export class DeleteSLO { }); } + private async deleteSummaryData(sloId: string): Promise { + await this.esClient.deleteByQuery({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + wait_for_completion: false, + query: { + match: { + 'slo.id': sloId, + }, + }, + }); + } private async deleteAssociatedRules(sloId: string): Promise { try { await this.rulesClient.bulkDeleteRules({ 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 84f22c86a923cf..0c5f6ea037b591 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,37 +5,45 @@ * 2.0. */ -import { SLO, SLOId, Summary } from '../../domain/models'; +import { SLO } from '../../domain/models'; import { FindSLO } from './find_slo'; -import { createSLO, createPaginatedSLO } from './fixtures/slo'; -import { createSummaryClientMock, createSLORepositoryMock } from './mocks'; -import { SLORepository, SortField, SortDirection } from './slo_repository'; -import { SummaryClient } from './summary_client'; +import { createSLO } from './fixtures/slo'; +import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks'; +import { SLORepository } from './slo_repository'; +import { Paginated, SLOSummary, SummarySearchClient } from './summary_search_client'; describe('FindSLO', () => { let mockRepository: jest.Mocked; - let mockSummaryClient: jest.Mocked; + let mockSummarySearchClient: jest.Mocked; let findSLO: FindSLO; beforeEach(() => { mockRepository = createSLORepositoryMock(); - mockSummaryClient = createSummaryClientMock(); - findSLO = new FindSLO(mockRepository, mockSummaryClient); + mockSummarySearchClient = createSummarySearchClientMock(); + findSLO = new FindSLO(mockRepository, mockSummarySearchClient); }); describe('happy path', () => { it('returns the results with pagination', async () => { const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); + mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo)); + mockRepository.findAllByIds.mockResolvedValueOnce([slo]); const result = await findSLO.execute({}); - expect(mockRepository.find).toHaveBeenCalledWith( - { name: undefined }, - { field: SortField.CreationTime, direction: SortDirection.Asc }, - { page: 1, perPage: 25 } - ); + expect(mockSummarySearchClient.search.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "", + Object { + "direction": "asc", + "field": "status", + }, + Object { + "page": 1, + "perPage": 25, + }, + ] + `); expect(result).toEqual({ page: 1, @@ -89,131 +97,65 @@ describe('FindSLO', () => { }); }); - it('calls the repository with the default criteria and pagination', async () => { + it('calls the repository with all the summary slo ids', async () => { const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); + mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo)); + mockRepository.findAllByIds.mockResolvedValueOnce([slo]); await findSLO.execute({}); - expect(mockRepository.find).toHaveBeenCalledWith( - { name: undefined }, - { field: SortField.CreationTime, direction: SortDirection.Asc }, - { page: 1, perPage: 25 } - ); - }); - - it('calls the repository with the name filter criteria', async () => { - const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); - - await findSLO.execute({ name: 'Availability' }); - - expect(mockRepository.find).toHaveBeenCalledWith( - { name: 'Availability' }, - { field: SortField.CreationTime, direction: SortDirection.Asc }, - { page: 1, perPage: 25 } - ); + expect(mockRepository.findAllByIds).toHaveBeenCalledWith([slo.id]); }); - it('calls the repository with the indicatorType filter criteria', async () => { + it('searches with the provided criteria', async () => { const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); - - await findSLO.execute({ indicatorTypes: ['sli.kql.custom'] }); - - expect(mockRepository.find).toHaveBeenCalledWith( - { indicatorTypes: ['sli.kql.custom'] }, - { field: SortField.CreationTime, direction: SortDirection.Asc }, - { page: 1, perPage: 25 } - ); - }); - - it('calls the repository with the pagination', async () => { - const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); - - await findSLO.execute({ name: 'My SLO*', page: '2', perPage: '100' }); - - expect(mockRepository.find).toHaveBeenCalledWith( - { name: 'My SLO*' }, - { field: SortField.CreationTime, direction: SortDirection.Asc }, - { page: 2, perPage: 100 } - ); - }); - - it('uses default pagination values when invalid', async () => { - const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); - - await findSLO.execute({ page: '-1', perPage: '0' }); - - expect(mockRepository.find).toHaveBeenCalledWith( - { name: undefined }, - { field: SortField.CreationTime, direction: SortDirection.Asc }, - { page: 1, perPage: 25 } - ); - }); - - it('sorts by name by default when not specified', async () => { - const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); - - await findSLO.execute({ sortBy: undefined }); - - expect(mockRepository.find).toHaveBeenCalledWith( - { name: undefined }, - { field: SortField.CreationTime, direction: SortDirection.Asc }, - { page: 1, perPage: 25 } - ); - }); - - it('sorts by indicator type', async () => { - const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); - - await findSLO.execute({ sortBy: 'indicatorType' }); - - expect(mockRepository.find).toHaveBeenCalledWith( - { name: undefined }, - { field: SortField.IndicatorType, direction: SortDirection.Asc }, - { page: 1, perPage: 25 } - ); - }); - - it('sorts by indicator type in descending order', async () => { - const slo = createSLO(); - mockRepository.find.mockResolvedValueOnce(createPaginatedSLO(slo)); - mockSummaryClient.fetchSummary.mockResolvedValueOnce(someSummary(slo)); - - await findSLO.execute({ sortBy: 'indicatorType', sortDirection: 'desc' }); + mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo)); + mockRepository.findAllByIds.mockResolvedValueOnce([slo]); + + await findSLO.execute({ + kqlQuery: "slo.name:'Service*' and slo.indicator.type:'sli.kql.custom'", + page: '2', + perPage: '10', + sortBy: 'error_budget_consumed', + sortDirection: 'asc', + }); - expect(mockRepository.find).toHaveBeenCalledWith( - { name: undefined }, - { field: SortField.IndicatorType, direction: SortDirection.Desc }, - { page: 1, perPage: 25 } - ); + expect(mockSummarySearchClient.search.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "slo.name:'Service*' and slo.indicator.type:'sli.kql.custom'", + Object { + "direction": "asc", + "field": "error_budget_consumed", + }, + Object { + "page": 2, + "perPage": 10, + }, + ] + `); }); }); }); -function someSummary(slo: SLO): Record { +function summarySearchResult(slo: SLO): Paginated { return { - [slo.id]: { - status: 'HEALTHY', - sliValue: 0.9999, - errorBudget: { - initial: 0.001, - consumed: 0.1, - remaining: 0.9, - isEstimated: false, + total: 1, + perPage: 25, + page: 1, + results: [ + { + id: slo.id, + summary: { + status: 'HEALTHY', + sliValue: 0.9999, + errorBudget: { + initial: 0.001, + consumed: 0.1, + remaining: 0.9, + isEstimated: false, + }, + }, }, - }, + ], }; } 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 1a21be1a416e85..ccbbe94a062266 100644 --- a/x-pack/plugins/observability/server/services/slo/find_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/find_slo.ts @@ -6,55 +6,45 @@ */ import { FindSLOParams, FindSLOResponse, findSLOResponseSchema } from '@kbn/slo-schema'; -import { SLO, SLOId, SLOWithSummary, Summary } from '../../domain/models'; -import { - Criteria, - Paginated, - Pagination, - SLORepository, - Sort, - SortField, - SortDirection, -} from './slo_repository'; -import { SummaryClient } from './summary_client'; +import { SLO, SLOWithSummary } from '../../domain/models'; +import { SLORepository } from './slo_repository'; +import { Pagination, SLOSummary, Sort, SummarySearchClient } from './summary_search_client'; const DEFAULT_PAGE = 1; const DEFAULT_PER_PAGE = 25; export class FindSLO { - constructor(private repository: SLORepository, private summaryClient: SummaryClient) {} + constructor( + private repository: SLORepository, + private summarySearchClient: SummarySearchClient + ) {} public async execute(params: FindSLOParams): Promise { - const pagination: Pagination = toPagination(params); - const criteria: Criteria = toCriteria(params); - const sort: Sort = toSort(params); - - const { results: sloList, ...resultMeta }: Paginated = await this.repository.find( - criteria, - sort, - pagination + const sloSummaryList = await this.summarySearchClient.search( + params.kqlQuery ?? '', + toSort(params), + toPagination(params) ); - const summaryBySlo = await this.summaryClient.fetchSummary(sloList); - const sloListWithSummary = mergeSloWithSummary(sloList, summaryBySlo); + const sloList = await this.repository.findAllByIds(sloSummaryList.results.map((slo) => slo.id)); + const sloListWithSummary = mergeSloWithSummary(sloList, sloSummaryList.results); return findSLOResponseSchema.encode({ - page: resultMeta.page, - perPage: resultMeta.perPage, - total: resultMeta.total, + page: sloSummaryList.page, + perPage: sloSummaryList.perPage, + total: sloSummaryList.total, results: sloListWithSummary, }); } } -function mergeSloWithSummary( - sloList: SLO[], - summaryBySlo: Record -): SLOWithSummary[] { - return sloList.map((slo) => ({ - ...slo, - summary: summaryBySlo[slo.id], - })); +function mergeSloWithSummary(sloList: SLO[], sloSummaryList: SLOSummary[]): SLOWithSummary[] { + return sloSummaryList + .filter((sloSummary) => sloList.some((s) => s.id === sloSummary.id)) + .map((sloSummary) => ({ + ...sloList.find((s) => s.id === sloSummary.id)!, + summary: sloSummary.summary, + })); } function toPagination(params: FindSLOParams): Pagination { @@ -67,13 +57,9 @@ function toPagination(params: FindSLOParams): Pagination { }; } -function toCriteria(params: FindSLOParams): Criteria { - return { name: params.name, indicatorTypes: params.indicatorTypes }; -} - function toSort(params: FindSLOParams): Sort { return { - field: params.sortBy === 'indicatorType' ? SortField.IndicatorType : SortField.CreationTime, - direction: params.sortDirection === 'desc' ? SortDirection.Desc : SortDirection.Asc, + field: params.sortBy ?? 'status', + direction: params.sortDirection ?? 'asc', }; } diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts b/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts index a07eab2642e91c..26690b6680f180 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/duration.ts @@ -7,14 +7,14 @@ import { Duration, DurationUnit } from '../../../domain/models'; -export function oneQuarter(): Duration { - return new Duration(1, DurationUnit.Quarter); -} - export function thirtyDays(): Duration { return new Duration(30, DurationUnit.Day); } +export function ninetyDays(): Duration { + return new Duration(90, DurationUnit.Day); +} + export function oneMonth(): Duration { return new Duration(1, DurationUnit.Month); } 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 fc7cf292095c94..2f645f6879a631 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts @@ -5,12 +5,10 @@ * 2.0. */ +import { SavedObject } from '@kbn/core-saved-objects-server'; +import { CreateSLOParams, HistogramIndicator, sloSchema } from '@kbn/slo-schema'; import { cloneDeep } from 'lodash'; import { v1 as uuidv1 } from 'uuid'; -import { SavedObject } from '@kbn/core-saved-objects-server'; -import { sloSchema, CreateSLOParams, HistogramIndicator } from '@kbn/slo-schema'; - -import { SO_SLO_TYPE } from '../../../saved_objects'; import { APMTransactionDurationIndicator, APMTransactionErrorRateIndicator, @@ -22,9 +20,9 @@ import { SLO, StoredSLO, } from '../../../domain/models'; -import { Paginated } from '../slo_repository'; -import { oneWeek, twoMinute } from './duration'; -import { sevenDaysRolling } from './time_window'; +import { SO_SLO_TYPE } from '../../../saved_objects'; +import { twoMinute } from './duration'; +import { sevenDaysRolling, weeklyCalendarAligned } from './time_window'; export const createAPMTransactionErrorRateIndicator = ( params: Partial = {} @@ -184,23 +182,7 @@ export const createSLOWithTimeslicesBudgetingMethod = (params: Partial = {} export const createSLOWithCalendarTimeWindow = (params: Partial = {}): SLO => { return createSLO({ - timeWindow: { - duration: oneWeek(), - type: 'calendarAligned', - }, + timeWindow: weeklyCalendarAligned(), ...params, }); }; - -export const createPaginatedSLO = ( - slo: SLO, - params: Partial> = {} -): Paginated => { - return { - page: 1, - perPage: 25, - total: 1, - results: [slo], - ...params, - }; -}; diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/summary_search_document.ts b/x-pack/plugins/observability/server/services/slo/fixtures/summary_search_document.ts new file mode 100644 index 00000000000000..2d2f1c262f8b47 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/fixtures/summary_search_document.ts @@ -0,0 +1,71 @@ +/* + * 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 { v1 as uuidv1 } from 'uuid'; + +export const aSummaryDocument = ({ + id = uuidv1(), + sliValue = 0.9, + consumed = 0.4, + isTempDoc = false, + status = 'HEALTHY', +} = {}) => { + return { + goodEvents: 96, + totalEvents: 100, + errorBudgetEstimated: false, + errorBudgetRemaining: 1 - consumed, + errorBudgetConsumed: consumed, + isTempDoc, + service: { + environment: null, + name: null, + }, + slo: { + indicator: { + type: 'sli.kql.custom', + }, + timeWindow: { + duration: '30d', + type: 'rolling', + }, + instanceId: '*', + name: 'irrelevant', + description: '', + id, + budgetingMethod: 'occurrences', + revision: 1, + tags: ['tag-one', 'tag-two', 'irrelevant'], + }, + errorBudgetInitial: 0.02, + transaction: { + name: null, + type: null, + }, + sliValue, + statusCode: 4, + status, + }; +}; + +export const aHitFromSummaryIndex = (_source: any) => { + return { + _index: '.slo-observability.summary-v2', + _id: uuidv1(), + _score: 1, + _source, + }; +}; + +export const aHitFromTempSummaryIndex = (_source: any) => { + return { + _index: '.slo-observability.summary-v2.temp', + _id: uuidv1(), + _score: 1, + _source, + }; +}; diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts b/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts index e2ffa8459720a4..c8d1601a22e2bc 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/time_window.ts @@ -5,15 +5,12 @@ * 2.0. */ -import { RollingTimeWindow, TimeWindow } from '../../../domain/models/time_window'; -import { oneWeek, sevenDays, sixHours, thirtyDays } from './duration'; - -export function sixHoursRolling(): TimeWindow { - return { - duration: sixHours(), - type: 'rolling', - }; -} +import { + CalendarAlignedTimeWindow, + RollingTimeWindow, + TimeWindow, +} from '../../../domain/models/time_window'; +import { ninetyDays, oneMonth, oneWeek, sevenDays, thirtyDays } from './duration'; export function sevenDaysRolling(): RollingTimeWindow { return { @@ -28,9 +25,23 @@ export function thirtyDaysRolling(): RollingTimeWindow { }; } -export function weeklyCalendarAligned(): TimeWindow { +export function ninetyDaysRolling(): TimeWindow { + return { + duration: ninetyDays(), + type: 'rolling', + }; +} + +export function weeklyCalendarAligned(): CalendarAlignedTimeWindow { return { duration: oneWeek(), type: 'calendarAligned', }; } + +export function monthlyCalendarAligned(): CalendarAlignedTimeWindow { + return { + duration: oneMonth(), + type: 'calendarAligned', + }; +} diff --git a/x-pack/plugins/observability/server/services/slo/get_diagnosis.ts b/x-pack/plugins/observability/server/services/slo/get_diagnosis.ts index a70b65ab895024..028a8089658d1f 100644 --- a/x-pack/plugins/observability/server/services/slo/get_diagnosis.ts +++ b/x-pack/plugins/observability/server/services/slo/get_diagnosis.ts @@ -6,18 +6,20 @@ */ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; - import { getSLOTransformId, SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_INDEX_TEMPLATE_NAME, SLO_INGEST_PIPELINE_NAME, + SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME, + SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME, + SLO_SUMMARY_INDEX_TEMPLATE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, } from '../../assets/constants'; -import { StoredSLO } from '../../domain/models'; -import { SO_SLO_TYPE } from '../../saved_objects'; +import { SLO } from '../../domain/models'; +import { SLORepository } from './slo_repository'; const OK = 'OK'; const NOT_OK = 'NOT_OK'; @@ -30,11 +32,19 @@ export async function getGlobalDiagnosis( const licenseInfo = licensing.license.toJSON(); const userPrivileges = await esClient.security.getUserPrivileges(); const sloResources = await getSloResourcesDiagnosis(esClient); + const sloSummaryResources = await getSloSummaryResourcesDiagnosis(esClient); + + const sloSummaryTransformsStats = await esClient.transform.getTransformStats({ + transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`, + allow_no_match: true, + }); return { licenseAndFeatures: licenseInfo, userPrivileges, sloResources, + sloSummaryResources, + sloSummaryTransformsStats, }; } catch (error) { throw error; @@ -43,42 +53,36 @@ export async function getGlobalDiagnosis( export async function getSloDiagnosis( sloId: string, - services: { esClient: ElasticsearchClient; soClient: SavedObjectsClientContract } + services: { esClient: ElasticsearchClient; repository: SLORepository } ) { - const { esClient, soClient } = services; + const { esClient, repository } = services; const sloResources = await getSloResourcesDiagnosis(esClient); + const sloSummaryResources = await getSloSummaryResourcesDiagnosis(esClient); - let sloSavedObject; + let slo: SLO | undefined; try { - sloSavedObject = await soClient.get(SO_SLO_TYPE, sloId); + slo = await repository.findById(sloId); } catch (err) { // noop } const sloTransformStats = await esClient.transform.getTransformStats({ - transform_id: getSLOTransformId(sloId, sloSavedObject?.attributes.revision ?? 1), + transform_id: getSLOTransformId(sloId, slo?.revision ?? 1), + allow_no_match: true, }); - let dataSample; - if (sloSavedObject?.attributes.indicator.params.index) { - const slo = sloSavedObject.attributes; - const sortField = - 'timestampField' in slo.indicator.params - ? slo.indicator.params.timestampField ?? '@timestamp' - : '@timestamp'; - dataSample = await esClient.search({ - index: slo.indicator.params.index, - sort: { [sortField]: 'desc' }, - size: 5, - }); - } + const sloSummaryTransformsStats = await esClient.transform.getTransformStats({ + transform_id: `${SLO_SUMMARY_TRANSFORM_NAME_PREFIX}*`, + allow_no_match: true, + }); return { sloResources, - sloSavedObject: sloSavedObject ?? NOT_OK, + sloSummaryResources, + slo: slo ?? NOT_OK, sloTransformStats, - dataSample: dataSample ?? NOT_OK, + sloSummaryTransformsStats, }; } @@ -116,3 +120,29 @@ async function getSloResourcesDiagnosis(esClient: ElasticsearchClient) { } } } + +async function getSloSummaryResourcesDiagnosis(esClient: ElasticsearchClient) { + try { + const indexTemplateExists = await esClient.indices.existsIndexTemplate({ + name: SLO_SUMMARY_INDEX_TEMPLATE_NAME, + }); + + const mappingsTemplateExists = await esClient.cluster.existsComponentTemplate({ + name: SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME, + }); + + const settingsTemplateExists = await esClient.cluster.existsComponentTemplate({ + name: SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME, + }); + + return { + [SLO_SUMMARY_INDEX_TEMPLATE_NAME]: indexTemplateExists ? OK : NOT_OK, + [SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME]: mappingsTemplateExists ? OK : NOT_OK, + [SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME]: settingsTemplateExists ? OK : NOT_OK, + }; + } catch (err) { + if (err.meta.statusCode === 403) { + throw new Error('Insufficient permissions to access Elasticsearch Cluster', { cause: err }); + } + } +} diff --git a/x-pack/plugins/observability/server/services/slo/index.ts b/x-pack/plugins/observability/server/services/slo/index.ts index 9d7097d92fc031..b2d08c0261783b 100644 --- a/x-pack/plugins/observability/server/services/slo/index.ts +++ b/x-pack/plugins/observability/server/services/slo/index.ts @@ -12,6 +12,8 @@ export * from './find_slo'; 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'; 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 bb8b66542b1db7..6c464a135a9729 100644 --- a/x-pack/plugins/observability/server/services/slo/mocks/index.ts +++ b/x-pack/plugins/observability/server/services/slo/mocks/index.ts @@ -9,6 +9,8 @@ import { ResourceInstaller } from '../resource_installer'; 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 => { @@ -17,6 +19,12 @@ const createResourceInstallerMock = (): jest.Mocked => { }; }; +const createSummaryTransformInstallerMock = (): jest.Mocked => { + return { + installAndStart: jest.fn(), + }; +}; + const createTransformManagerMock = (): jest.Mocked => { return { install: jest.fn(), @@ -32,7 +40,6 @@ const createSLORepositoryMock = (): jest.Mocked => { findById: jest.fn(), findAllByIds: jest.fn(), deleteById: jest.fn(), - find: jest.fn(), }; }; @@ -42,6 +49,12 @@ const createSummaryClientMock = (): jest.Mocked => { }; }; +const createSummarySearchClientMock = (): jest.Mocked => { + return { + search: jest.fn(), + }; +}; + const createSLIClientMock = (): jest.Mocked => { return { fetchSLIDataFrom: jest.fn(), @@ -50,8 +63,10 @@ const createSLIClientMock = (): jest.Mocked => { export { createResourceInstallerMock, + createSummaryTransformInstallerMock, createTransformManagerMock, createSLORepositoryMock, createSummaryClientMock, + createSummarySearchClientMock, createSLIClientMock, }; 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 90749176513da8..42add5a5395662 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 @@ -5,28 +5,31 @@ * 2.0. */ -import { IngestGetPipelineResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { - SLO_INGEST_PIPELINE_NAME, SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_INDEX_TEMPLATE_NAME, + SLO_INGEST_PIPELINE_NAME, SLO_RESOURCES_VERSION, + SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME, + SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME, + SLO_SUMMARY_INDEX_TEMPLATE_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, } from '../../assets/constants'; import { DefaultResourceInstaller } from './resource_installer'; describe('resourceInstaller', () => { - describe("when the common resources don't exist", () => { + describe('when the common resources are not installed yet', () => { it('installs the common resources', async () => { const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(false); + mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ index_templates: [] }); const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); await installer.ensureCommonResourcesInstalled(); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(2); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( 1, expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME }) @@ -35,23 +38,71 @@ describe('resourceInstaller', () => { 2, expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_SETTINGS_NAME }) ); - expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledWith( + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME }) + ); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 4, + expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME }) + ); + expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(2); + expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 1, expect.objectContaining({ name: SLO_INDEX_TEMPLATE_NAME }) ); - expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledWith( + expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ name: SLO_SUMMARY_INDEX_TEMPLATE_NAME }) + ); + + expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(2); + 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 }) + ); }); }); - describe('when the common resources exist', () => { - it('does not install the common resources', async () => { + describe('when the common resources are already installed', () => { + it('skips the installation', async () => { const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(true); + mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ + index_templates: [ + { + name: SLO_INDEX_TEMPLATE_NAME, + index_template: { + index_patterns: [], + composed_of: [], + _meta: { version: SLO_RESOURCES_VERSION }, + }, + }, + ], + }); + mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ + index_templates: [ + { + name: SLO_SUMMARY_INDEX_TEMPLATE_NAME, + index_template: { + index_patterns: [], + composed_of: [], + _meta: { version: SLO_RESOURCES_VERSION }, + }, + }, + ], + }); mockClusterClient.ingest.getPipeline.mockResponseOnce({ // @ts-ignore _meta not typed properly [SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } }, - } as IngestGetPipelineResponse); + }); + mockClusterClient.ingest.getPipeline.mockResponseOnce({ + // @ts-ignore _meta not typed properly + [SLO_SUMMARY_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } }, + }); const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); await installer.ensureCommonResourcesInstalled(); 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 abc02052097b5b..742b2a8e7202ea 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.ts @@ -11,18 +11,32 @@ import type { IngestPutPipelineRequest, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; - +import { getSLOMappingsTemplate } from '../../assets/component_templates/slo_mappings_template'; +import { getSLOSettingsTemplate } from '../../assets/component_templates/slo_settings_template'; +import { getSLOSummaryMappingsTemplate } from '../../assets/component_templates/slo_summary_mappings_template'; +import { getSLOSummarySettingsTemplate } from '../../assets/component_templates/slo_summary_settings_template'; import { - SLO_INGEST_PIPELINE_NAME, SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, + SLO_DESTINATION_INDEX_NAME, SLO_INDEX_TEMPLATE_NAME, + SLO_INDEX_TEMPLATE_PATTERN, + SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX, + SLO_INGEST_PIPELINE_NAME, SLO_RESOURCES_VERSION, + SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME, + SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME, + 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 '../../assets/constants'; -import { getSLOMappingsTemplate } from '../../assets/component_templates/slo_mappings_template'; -import { getSLOSettingsTemplate } from '../../assets/component_templates/slo_settings_template'; 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 { ensureCommonResourcesInstalled(): Promise; @@ -35,13 +49,12 @@ export class DefaultResourceInstaller implements ResourceInstaller { const alreadyInstalled = await this.areResourcesAlreadyInstalled(); if (alreadyInstalled) { - this.logger.debug( - `Skipping installation of resources shared for SLO since they already exist` - ); + this.logger.info('SLO resources already installed - skipping'); return; } try { + this.logger.info('Installing SLO shared resources'); await Promise.all([ this.createOrUpdateComponentTemplate( getSLOMappingsTemplate(SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME) @@ -49,41 +62,90 @@ export class DefaultResourceInstaller implements ResourceInstaller { this.createOrUpdateComponentTemplate( getSLOSettingsTemplate(SLO_COMPONENT_TEMPLATE_SETTINGS_NAME) ), + this.createOrUpdateComponentTemplate( + getSLOSummaryMappingsTemplate(SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME) + ), + this.createOrUpdateComponentTemplate( + getSLOSummarySettingsTemplate(SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME) + ), ]); await this.createOrUpdateIndexTemplate( - getSLOIndexTemplate(SLO_INDEX_TEMPLATE_NAME, `${SLO_INDEX_TEMPLATE_NAME}-*`, [ + getSLOIndexTemplate(SLO_INDEX_TEMPLATE_NAME, SLO_INDEX_TEMPLATE_PATTERN, [ SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, ]) ); - await this.createOrUpdateIngestPipelineTemplate( - getSLOPipelineTemplate( - SLO_INGEST_PIPELINE_NAME, - this.getPipelinePrefix(SLO_RESOURCES_VERSION) + await this.createOrUpdateIndexTemplate( + getSLOSummaryIndexTemplate( + SLO_SUMMARY_INDEX_TEMPLATE_NAME, + SLO_SUMMARY_INDEX_TEMPLATE_PATTERN, + [ + SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME, + SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME, + ] ) ); + + await this.createIndex(SLO_DESTINATION_INDEX_NAME); + await this.createIndex(SLO_SUMMARY_DESTINATION_INDEX_NAME); + await this.createIndex(SLO_SUMMARY_TEMP_INDEX_NAME); + + 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}`); + this.logger.error(`Error installing resources shared for SLO: ${err.message}`); throw err; } } - private getPipelinePrefix(version: number): string { - // Following https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme - // slo-observability.sli-. - return `${SLO_INDEX_TEMPLATE_NAME}-v${version}.`; - } - private async areResourcesAlreadyInstalled(): Promise { - const indexTemplateExists = await this.esClient.indices.existsIndexTemplate({ - name: SLO_INDEX_TEMPLATE_NAME, - }); + let indexTemplateExists = false; + try { + const { index_templates: indexTemplates } = await this.execute(() => + this.esClient.indices.getIndexTemplate({ + name: SLO_INDEX_TEMPLATE_NAME, + }) + ); + + const sloIndexTemplate = indexTemplates.find( + (template) => template.name === SLO_INDEX_TEMPLATE_NAME + ); + indexTemplateExists = + !!sloIndexTemplate && + sloIndexTemplate.index_template._meta?.version === SLO_RESOURCES_VERSION; + } catch (err) { + return false; + } + + let summaryIndexTemplateExists = false; + try { + const { index_templates: indexTemplates } = await this.execute(() => + this.esClient.indices.getIndexTemplate({ + name: SLO_SUMMARY_INDEX_TEMPLATE_NAME, + }) + ); + const sloSummaryIndexTemplate = indexTemplates.find( + (template) => template.name === SLO_SUMMARY_INDEX_TEMPLATE_NAME + ); + summaryIndexTemplateExists = + !!sloSummaryIndexTemplate && + sloSummaryIndexTemplate.index_template._meta?.version === SLO_RESOURCES_VERSION; + } catch (err) { + return false; + } let ingestPipelineExists = false; try { - const pipeline = await this.esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME }); + const pipeline = await this.execute(() => + this.esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME }) + ); ingestPipelineExists = // @ts-ignore _meta is not defined on the type @@ -92,21 +154,54 @@ export class DefaultResourceInstaller implements ResourceInstaller { return false; } - return indexTemplateExists && ingestPipelineExists; + let summaryIngestPipelineExists = false; + try { + const pipeline = await this.execute(() => + this.esClient.ingest.getPipeline({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME }) + ); + + summaryIngestPipelineExists = + pipeline && + // @ts-ignore _meta is not defined on the type + pipeline[SLO_SUMMARY_INGEST_PIPELINE_NAME]._meta.version === SLO_RESOURCES_VERSION; + } catch (err) { + return false; + } + + return ( + indexTemplateExists && + summaryIndexTemplateExists && + ingestPipelineExists && + summaryIngestPipelineExists + ); } private async createOrUpdateComponentTemplate(template: ClusterPutComponentTemplateRequest) { - this.logger.debug(`Installing SLO component template ${template.name}`); - return this.esClient.cluster.putComponentTemplate(template); + this.logger.info(`Installing SLO component template [${template.name}]`); + return this.execute(() => this.esClient.cluster.putComponentTemplate(template)); } private async createOrUpdateIndexTemplate(template: IndicesPutIndexTemplateRequest) { - this.logger.debug(`Installing SLO index template ${template.name}`); - return this.esClient.indices.putIndexTemplate(template); + this.logger.info(`Installing SLO index template [${template.name}]`); + return this.execute(() => this.esClient.indices.putIndexTemplate(template)); } private async createOrUpdateIngestPipelineTemplate(template: IngestPutPipelineRequest) { - this.logger.debug(`Installing SLO ingest pipeline template ${template.id}`); - await this.esClient.ingest.putPipeline(template); + this.logger.info(`Installing SLO ingest pipeline [${template.id}]`); + return this.execute(() => this.esClient.ingest.putPipeline(template)); + } + + private async createIndex(indexName: string) { + try { + await this.execute(() => this.esClient.indices.create({ index: indexName })); + } catch (err) { + if (err?.meta?.body?.error?.type !== 'resource_already_exists_exception') { + throw err; + } + } + } + + private async execute(esCall: () => Promise): Promise { + return await retryTransientEsErrors(esCall, { logger: this.logger }); } } diff --git a/x-pack/plugins/observability/server/services/slo/sli_client.ts b/x-pack/plugins/observability/server/services/slo/sli_client.ts index c40ca6d216facf..1a9e6f46ec82cf 100644 --- a/x-pack/plugins/observability/server/services/slo/sli_client.ts +++ b/x-pack/plugins/observability/server/services/slo/sli_client.ts @@ -13,11 +13,15 @@ import { MsearchMultisearchBody, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient } from '@kbn/core/server'; -import { occurrencesBudgetingMethodSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; +import { + occurrencesBudgetingMethodSchema, + timeslicesBudgetingMethodSchema, + toMomentUnitOfTime, +} from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; +import moment from 'moment'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../assets/constants'; import { DateRange, Duration, IndicatorData, SLO } from '../../domain/models'; -import { toDateRange } from '../../domain/services/date_range'; import { InternalQueryError } from '../../errors'; export interface SLIClient { @@ -47,10 +51,7 @@ export class DefaultSLIClient implements SLIClient { a.duration.isShorterThan(b.duration) ? 1 : -1 ); const longestLookbackWindow = sortedLookbackWindows[0]; - const longestDateRange = toDateRange({ - duration: longestLookbackWindow.duration, - type: 'rolling', - }); + const longestDateRange = getLookbackDateRange(longestLookbackWindow.duration); if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) { const result = await this.esClient.search({ @@ -179,3 +180,15 @@ function handleWindowedResult( return indicatorDataPerLookbackWindow; } + +function getLookbackDateRange(duration: Duration): { from: Date; to: Date } { + const unit = toMomentUnitOfTime(duration.unit); + const now = moment.utc().startOf('minute'); + const from = now.clone().subtract(duration.value, unit); + const to = now.clone(); + + return { + from: from.toDate(), + to: to.toDate(), + }; +} 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 new file mode 100644 index 00000000000000..6bd9f798c62340 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/slo_installer.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { loggingSystemMock } from '@kbn/core/server/mocks'; +import { MockedLogger } from '@kbn/logging-mocks'; +import { createResourceInstallerMock, createSummaryTransformInstallerMock } from './mocks'; +import { DefaultSLOInstaller } from './slo_installer'; + +describe('SLO Installer', () => { + let loggerMock: jest.Mocked; + + beforeEach(() => { + loggerMock = loggingSystemMock.createLogger(); + }); + + it.skip('handles concurrent installation', async () => { + const resourceInstaller = createResourceInstallerMock(); + const summaryTransformInstaller = createSummaryTransformInstallerMock(); + const service = new DefaultSLOInstaller( + resourceInstaller, + summaryTransformInstaller, + 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 new file mode 100644 index 00000000000000..b59612ca302531 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/slo_installer.ts @@ -0,0 +1,46 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import { ResourceInstaller, SummaryTransformInstaller } from '.'; + +export interface SLOInstaller { + install(): Promise; +} + +export class DefaultSLOInstaller implements SLOInstaller { + private isInstalling: boolean = false; + + constructor( + private sloResourceInstaller: ResourceInstaller, + private sloSummaryInstaller: SummaryTransformInstaller, + private logger: Logger + ) {} + + public async install() { + if (this.isInstalling || process.env.CI) { + return; + } + this.isInstalling = true; + + let installTimeout; + try { + 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', { + error, + }); + throw error; + } 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 9149f58cc28c04..e303b759ea4ef3 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,23 +8,16 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { sloSchema } from '@kbn/slo-schema'; - import { SLO, StoredSLO } from '../../domain/models'; -import { SO_SLO_TYPE } from '../../saved_objects'; -import { - KibanaSavedObjectsSLORepository, - Pagination, - Sort, - SortDirection, - SortField, -} from './slo_repository'; -import { createAPMTransactionDurationIndicator, createSLO, aStoredSLO } from './fixtures/slo'; import { SLOIdConflict, SLONotFound } from '../../errors'; +import { SO_SLO_TYPE } from '../../saved_objects'; +import { aStoredSLO, createAPMTransactionDurationIndicator, createSLO } from './fixtures/slo'; +import { KibanaSavedObjectsSLORepository } from './slo_repository'; const SOME_SLO = createSLO({ indicator: createAPMTransactionDurationIndicator() }); const ANOTHER_SLO = createSLO(); -function createFindResponse(sloList: SLO[]): SavedObjectsFindResponse { +function soFindResponse(sloList: SLO[]): SavedObjectsFindResponse { return { page: 1, per_page: 25, @@ -48,7 +41,7 @@ describe('KibanaSavedObjectsSLORepository', () => { describe('validation', () => { it('findById throws when an SLO is not found', async () => { - soClientMock.find.mockResolvedValueOnce(createFindResponse([])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([])); const repository = new KibanaSavedObjectsSLORepository(soClientMock); await expect(repository.findById('inexistant-slo-id')).rejects.toThrowError( @@ -57,7 +50,7 @@ describe('KibanaSavedObjectsSLORepository', () => { }); it('deleteById throws when an SLO is not found', async () => { - soClientMock.find.mockResolvedValueOnce(createFindResponse([])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([])); const repository = new KibanaSavedObjectsSLORepository(soClientMock); await expect(repository.deleteById('inexistant-slo-id')).rejects.toThrowError( @@ -69,7 +62,7 @@ describe('KibanaSavedObjectsSLORepository', () => { describe('saving an SLO', () => { it('saves the new SLO', async () => { const slo = createSLO({ id: 'my-id' }); - soClientMock.find.mockResolvedValueOnce(createFindResponse([])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([])); soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo)); const repository = new KibanaSavedObjectsSLORepository(soClientMock); @@ -90,7 +83,7 @@ describe('KibanaSavedObjectsSLORepository', () => { it('throws when the SLO id already exists and "throwOnConflict" is true', async () => { const slo = createSLO({ id: 'my-id' }); - soClientMock.find.mockResolvedValueOnce(createFindResponse([slo])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([slo])); const repository = new KibanaSavedObjectsSLORepository(soClientMock); await expect(repository.save(slo, { throwOnConflict: true })).rejects.toThrowError( @@ -106,7 +99,7 @@ describe('KibanaSavedObjectsSLORepository', () => { it('updates the existing SLO', async () => { const slo = createSLO({ id: 'my-id' }); - soClientMock.find.mockResolvedValueOnce(createFindResponse([slo])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([slo])); soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo)); const repository = new KibanaSavedObjectsSLORepository(soClientMock); @@ -128,7 +121,7 @@ describe('KibanaSavedObjectsSLORepository', () => { it('finds an existing SLO', async () => { const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO])); const foundSLO = await repository.findById(SOME_SLO.id); @@ -143,7 +136,7 @@ describe('KibanaSavedObjectsSLORepository', () => { it('finds all SLOs by ids', async () => { const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO, ANOTHER_SLO])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO, ANOTHER_SLO])); const results = await repository.findAllByIds([SOME_SLO.id, ANOTHER_SLO.id]); @@ -158,7 +151,7 @@ describe('KibanaSavedObjectsSLORepository', () => { it('deletes an SLO', async () => { const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); + soClientMock.find.mockResolvedValueOnce(soFindResponse([SOME_SLO])); await repository.deleteById(SOME_SLO.id); @@ -170,238 +163,4 @@ describe('KibanaSavedObjectsSLORepository', () => { }); expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, SOME_SLO.id); }); - - describe('find', () => { - const DEFAULT_PAGINATION: Pagination = { page: 1, perPage: 25 }; - const DEFAULT_SORTING: Sort = { - field: SortField.CreationTime, - direction: SortDirection.Asc, - }; - - describe('Name search', () => { - it('includes the search on name with wildcard when provided', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - const result = await repository.find( - { name: 'availability*' }, - DEFAULT_SORTING, - DEFAULT_PAGINATION - ); - - expect(result).toEqual({ - page: 1, - perPage: 25, - total: 1, - results: [SOME_SLO], - }); - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - filter: undefined, - search: '*availability*', - searchFields: ['name'], - sortField: 'created_at', - sortOrder: 'asc', - }); - }); - - it('includes the search on name with added wildcard when not provided', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - const result = await repository.find( - { name: 'availa' }, - DEFAULT_SORTING, - DEFAULT_PAGINATION - ); - - expect(result).toEqual({ - page: 1, - perPage: 25, - total: 1, - results: [SOME_SLO], - }); - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - filter: undefined, - search: '*availa*', - searchFields: ['name'], - sortField: 'created_at', - sortOrder: 'asc', - }); - }); - }); - - describe('indicatorTypes filter', () => { - it('includes the filter on indicator types when provided', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - const result = await repository.find( - { indicatorTypes: ['sli.kql.custom'] }, - DEFAULT_SORTING, - DEFAULT_PAGINATION - ); - - expect(result).toEqual({ - page: 1, - perPage: 25, - total: 1, - results: [SOME_SLO], - }); - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - filter: `(slo.attributes.indicator.type: sli.kql.custom)`, - search: undefined, - searchFields: undefined, - sortField: 'created_at', - sortOrder: 'asc', - }); - }); - - it('includes the filter on indicator types as logical OR when provided', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - const result = await repository.find( - { indicatorTypes: ['sli.kql.custom', 'sli.apm.transactionDuration'] }, - DEFAULT_SORTING, - DEFAULT_PAGINATION - ); - - expect(result).toEqual({ - page: 1, - perPage: 25, - total: 1, - results: [SOME_SLO], - }); - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - filter: `(slo.attributes.indicator.type: sli.kql.custom or slo.attributes.indicator.type: sli.apm.transactionDuration)`, - search: undefined, - searchFields: undefined, - sortField: 'created_at', - sortOrder: 'asc', - }); - }); - }); - - it('includes search on name and filter on indicator types', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - const result = await repository.find( - { name: 'latency', indicatorTypes: ['sli.kql.custom', 'sli.apm.transactionDuration'] }, - DEFAULT_SORTING, - DEFAULT_PAGINATION - ); - - expect(result).toEqual({ - page: 1, - perPage: 25, - total: 1, - results: [SOME_SLO], - }); - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - filter: `(slo.attributes.indicator.type: sli.kql.custom or slo.attributes.indicator.type: sli.apm.transactionDuration)`, - search: '*latency*', - searchFields: ['name'], - sortField: 'created_at', - sortOrder: 'asc', - }); - }); - - it('does not include the search or filter when no criteria provided', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - const result = await repository.find({}, DEFAULT_SORTING, DEFAULT_PAGINATION); - - expect(result).toEqual({ - page: 1, - perPage: 25, - total: 1, - results: [SOME_SLO], - }); - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - search: undefined, - searchFields: undefined, - sortField: 'created_at', - sortOrder: 'asc', - }); - }); - - it('sorts by creation time ascending', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - await repository.find({}, DEFAULT_SORTING, DEFAULT_PAGINATION); - - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - search: undefined, - searchFields: undefined, - sortField: 'created_at', - sortOrder: 'asc', - }); - }); - - it('sorts by creation time descending', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - await repository.find( - {}, - { field: SortField.CreationTime, direction: SortDirection.Desc }, - DEFAULT_PAGINATION - ); - - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - search: undefined, - searchFields: undefined, - sortField: 'created_at', - sortOrder: 'desc', - }); - }); - - it('sorts by indicator type', async () => { - const repository = new KibanaSavedObjectsSLORepository(soClientMock); - soClientMock.find.mockResolvedValueOnce(createFindResponse([SOME_SLO])); - - await repository.find( - {}, - { field: SortField.IndicatorType, direction: SortDirection.Asc }, - DEFAULT_PAGINATION - ); - - expect(soClientMock.find).toHaveBeenCalledWith({ - type: SO_SLO_TYPE, - page: 1, - perPage: 25, - search: undefined, - searchFields: undefined, - sortField: 'indicator.type', - sortOrder: 'asc', - }); - }); - }); }); 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 b283dc05fca99e..6ab9a78dc49e3d 100644 --- a/x-pack/plugins/observability/server/services/slo/slo_repository.ts +++ b/x-pack/plugins/observability/server/services/slo/slo_repository.ts @@ -5,62 +5,21 @@ * 2.0. */ -import * as t from 'io-ts'; -import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { sloSchema } from '@kbn/slo-schema'; - -import { StoredSLO, SLO } from '../../domain/models'; -import { SO_SLO_TYPE } from '../../saved_objects'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as t from 'io-ts'; +import { SLO, StoredSLO } from '../../domain/models'; import { SLOIdConflict, SLONotFound } from '../../errors'; - -type ObjectValues = T[keyof T]; - -export interface Criteria { - name?: string; - indicatorTypes?: string[]; -} - -export interface Pagination { - page: number; - perPage: number; -} - -export const SortDirection = { - Asc: 'Asc', - Desc: 'Desc', -} as const; - -type SortDirection = ObjectValues; - -export const SortField = { - CreationTime: 'CreationTime', - IndicatorType: 'IndicatorType', -}; - -type SortField = ObjectValues; - -export interface Sort { - field: SortField; - direction: SortDirection; -} - -export interface Paginated { - page: number; - perPage: number; - total: number; - results: T[]; -} +import { SO_SLO_TYPE } from '../../saved_objects'; export interface SLORepository { save(slo: SLO, options?: { throwOnConflict: boolean }): Promise; findAllByIds(ids: string[]): Promise; findById(id: string): Promise; deleteById(id: string): Promise; - find(criteria: Criteria, sort: Sort, pagination: Pagination): Promise>; } export class KibanaSavedObjectsSLORepository implements SLORepository { @@ -120,29 +79,6 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { await this.soClient.delete(SO_SLO_TYPE, response.saved_objects[0].id); } - async find(criteria: Criteria, sort: Sort, pagination: Pagination): Promise> { - const { search, searchFields } = buildSearch(criteria); - const filterKuery = buildFilterKuery(criteria); - const { sortField, sortOrder } = buildSortQuery(sort); - const response = await this.soClient.find({ - type: SO_SLO_TYPE, - page: pagination.page, - perPage: pagination.perPage, - search, - searchFields, - filter: filterKuery, - sortField, - sortOrder, - }); - - return { - total: response.total, - page: response.page, - perPage: response.per_page, - results: response.saved_objects.map((so) => toSLO(so.attributes)), - }; - } - async findAllByIds(ids: string[]): Promise { if (ids.length === 0) return []; @@ -163,47 +99,6 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { } } -function buildSearch(criteria: Criteria): { - search: string | undefined; - searchFields: string[] | undefined; -} { - if (!criteria.name) { - return { search: undefined, searchFields: undefined }; - } - - return { search: addWildcardsIfAbsent(criteria.name), searchFields: ['name'] }; -} - -function buildFilterKuery(criteria: Criteria): string | undefined { - const filters: string[] = []; - if (!!criteria.indicatorTypes) { - const indicatorTypesFilter: string[] = criteria.indicatorTypes.map( - (indicatorType) => `slo.attributes.indicator.type: ${indicatorType}` - ); - filters.push(`(${indicatorTypesFilter.join(' or ')})`); - } - - return filters.length > 0 ? filters.join(' and ') : undefined; -} - -function buildSortQuery(sort: Sort): { sortField: string; sortOrder: 'asc' | 'desc' } { - let sortField: string; - switch (sort.field) { - case SortField.IndicatorType: - sortField = 'indicator.type'; - break; - case SortField.CreationTime: - default: - sortField = 'created_at'; - break; - } - - return { - sortField, - sortOrder: sort.direction === SortDirection.Desc ? 'desc' : 'asc', - }; -} - function toStoredSLO(slo: SLO): StoredSLO { return sloSchema.encode(slo); } @@ -216,17 +111,3 @@ function toSLO(storedSLO: StoredSLO): SLO { }, t.identity) ); } - -const WILDCARD_CHAR = '*'; -function addWildcardsIfAbsent(value: string): string { - let updatedValue = value; - if (updatedValue.substring(0, 1) !== WILDCARD_CHAR) { - updatedValue = `${WILDCARD_CHAR}${updatedValue}`; - } - - if (value.substring(value.length - 1) !== WILDCARD_CHAR) { - updatedValue = `${updatedValue}${WILDCARD_CHAR}`; - } - - return updatedValue; -} 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 new file mode 100644 index 00000000000000..fc53bf1e7181bf --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts @@ -0,0 +1,116 @@ +/* + * 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 } from '@kbn/core/server/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { + aHitFromSummaryIndex, + aHitFromTempSummaryIndex, + aSummaryDocument, +} from './fixtures/summary_search_document'; +import { + DefaultSummarySearchClient, + Pagination, + Sort, + SummarySearchClient, +} from './summary_search_client'; + +const defaultSort: Sort = { + field: 'sli_value', + direction: 'asc', +}; +const defaultPagination: Pagination = { + page: 1, + perPage: 20, +}; + +describe('Summary Search Client', () => { + let esClientMock: ElasticsearchClientMock; + let service: SummarySearchClient; + + beforeEach(() => { + esClientMock = elasticsearchServiceMock.createElasticsearchClient(); + service = new DefaultSummarySearchClient(esClientMock, loggerMock.create()); + }); + + it('returns an empty response on error', async () => { + esClientMock.count.mockRejectedValue(new Error('Cannot reach es')); + + await expect(service.search('', defaultSort, defaultPagination)).resolves + .toMatchInlineSnapshot(` + Object { + "page": 1, + "perPage": 20, + "results": Array [], + "total": 0, + } + `); + }); + + it('returns an empty response when the kql filter returns no document count', async () => { + esClientMock.count.mockResolvedValue({ + count: 0, + _shards: { failed: 0, successful: 1, total: 1 }, + }); + + await expect(service.search('', defaultSort, defaultPagination)).resolves + .toMatchInlineSnapshot(` + Object { + "page": 1, + "perPage": 20, + "results": Array [], + "total": 0, + } + `); + }); + + it('returns the summary documents without duplicate temporary summary documents', async () => { + const SLO_ID1 = 'slo-one'; + const SLO_ID2 = 'slo_two'; + const SLO_ID3 = 'slo-three'; + const SLO_ID4 = 'slo-four'; + const SLO_ID5 = 'slo-five'; + esClientMock.count.mockResolvedValue({ + count: 8, + _shards: { failed: 0, successful: 1, total: 1 }, + }); + esClientMock.search.mockResolvedValue({ + took: 0, + timed_out: false, + _shards: { + total: 2, + successful: 2, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 6, + relation: 'eq', + }, + max_score: 1, + hits: [ + aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID1 })), + aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID2 })), + aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID3 })), + aHitFromSummaryIndex(aSummaryDocument({ id: SLO_ID5 })), // no related temp doc + aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID1, isTempDoc: true })), // removed as dup + aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID2, isTempDoc: true })), // removed as dup + aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID3, isTempDoc: true })), // removed as dup + aHitFromTempSummaryIndex(aSummaryDocument({ id: SLO_ID4, isTempDoc: true })), // kept + ], + }, + }); + + const results = await service.search('', defaultSort, defaultPagination); + + expect(esClientMock.deleteByQuery).toHaveBeenCalled(); + expect(esClientMock.deleteByQuery.mock.calls[0]).toMatchSnapshot(); + expect(results).toMatchSnapshot(); + expect(results.total).toBe(5); + }); +}); 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 new file mode 100644 index 00000000000000..faf99af44838aa --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_search_client.ts @@ -0,0 +1,157 @@ +/* + * 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 { assertNever } from '@kbn/std'; +import _ from 'lodash'; +import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../assets/constants'; +import { SLOId, Status, Summary } from '../../domain/models'; +import { toHighPrecision } from '../../utils/number'; +import { getElastichsearchQueryOrThrow } from './transform_generators'; + +interface EsSummaryDocument { + slo: { + id: string; + revision: number; + }; + sliValue: number; + errorBudgetConsumed: number; + errorBudgetRemaining: number; + errorBudgetInitial: number; + errorBudgetEstimated: boolean; + statusCode: number; + status: Status; + isTempDoc: boolean; +} + +export interface Paginated { + total: number; + page: number; + perPage: number; + results: T[]; +} + +export interface SLOSummary { + id: SLOId; + summary: Summary; +} + +export type SortField = 'error_budget_consumed' | 'error_budget_remaining' | 'sli_value' | 'status'; +export interface Sort { + field: SortField; + 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) {} + + async search( + kqlQuery: string, + sort: Sort, + pagination: Pagination + ): Promise> { + try { + const { count: total } = await this.esClient.count({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + query: getElastichsearchQueryOrThrow(kqlQuery), + }); + + if (total === 0) { + return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] }; + } + + const summarySearch = await this.esClient.search({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + query: getElastichsearchQueryOrThrow(kqlQuery), + sort: { + // non-temp first, then temp documents + isTempDoc: { + order: 'asc', + }, + [toDocumentSortField(sort.field)]: { + order: sort.direction, + }, + }, + from: (pagination.page - 1) * pagination.perPage, + size: pagination.perPage * 2, // twice as much as we return, in case they are all duplicate temp/non-temp summary + }); + + const [tempSummaryDocuments, summaryDocuments] = _.partition( + summarySearch.hits.hits, + (doc) => !!doc._source?.isTempDoc + ); + + // Always attempt to delete temporary summary documents with an existing non-temp summary document + // The temp summary documents are _eventually_ removed as we get through the real summary documents + const summarySloIds = summaryDocuments.map((doc) => doc._source?.slo.id); + await this.esClient.deleteByQuery({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + wait_for_completion: false, + query: { + bool: { + filter: [{ terms: { 'slo.id': summarySloIds } }, { term: { isTempDoc: true } }], + }, + }, + }); + + const tempSummaryDocumentsDeduped = tempSummaryDocuments.filter( + (doc) => !summarySloIds.includes(doc._source?.slo.id) + ); + + const finalResults = summaryDocuments + .concat(tempSummaryDocumentsDeduped) + .slice(0, pagination.perPage); + + const finalTotal = total - (tempSummaryDocuments.length - tempSummaryDocumentsDeduped.length); + return { + total: finalTotal, + perPage: pagination.perPage, + page: pagination.page, + results: finalResults.map((doc) => ({ + id: doc._source!.slo.id, + summary: { + errorBudget: { + initial: toHighPrecision(doc._source!.errorBudgetInitial), + consumed: toHighPrecision(doc._source!.errorBudgetConsumed), + remaining: toHighPrecision(doc._source!.errorBudgetRemaining), + isEstimated: doc._source!.errorBudgetEstimated, + }, + sliValue: toHighPrecision(doc._source!.sliValue), + status: doc._source!.status, + }, + })), + }; + } catch (err) { + this.logger.error(new Error('Summary search query error', { cause: err })); + return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] }; + } + } +} + +function toDocumentSortField(field: SortField) { + switch (field) { + case 'error_budget_consumed': + return 'errorBudgetConsumed'; + case 'error_budget_remaining': + return 'errorBudgetRemaining'; + case 'status': + return 'status'; + case 'sli_value': + return 'sliValue'; + default: + assertNever(field); + } +} 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 new file mode 100644 index 00000000000000..15050254a24ee8 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/__snapshots__/summary_transform_installer.test.ts.snap @@ -0,0 +1,1162 @@ +// 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": 2, + }, + "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 { 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.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, + }, + "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": 2, + }, + "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 { 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.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, + }, + "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": 2, + }, + "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 { 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.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, + }, + "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": 2, + }, + "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 { 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.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, + }, + "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": 2, + }, + "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 { 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.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, + }, + "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/helpers/create_temp_summary.ts b/x-pack/plugins/observability/server/services/slo/summary_transform/helpers/create_temp_summary.ts new file mode 100644 index 00000000000000..84d87418519efd --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/helpers/create_temp_summary.ts @@ -0,0 +1,47 @@ +/* + * 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 { SLO } from '../../../../domain/models'; + +export function createTempSummaryDocument(slo: SLO) { + return { + service: { + environment: null, + name: null, + }, + transaction: { + name: null, + type: null, + }, + slo: { + indicator: { + type: slo.indicator.type, + }, + timeWindow: { + duration: slo.timeWindow.duration.format(), + type: slo.timeWindow.type, + }, + instanceId: '*', + name: slo.name, + description: slo.description, + id: slo.id, + budgetingMethod: slo.budgetingMethod, + revision: slo.revision, + tags: slo.tags, + }, + goodEvents: 0, + totalEvents: 0, + errorBudgetEstimated: false, + errorBudgetRemaining: 1, + errorBudgetConsumed: 0, + errorBudgetInitial: 1 - slo.objective.target, + sliValue: -1, + statusCode: 0, + status: 'NO_DATA', + isTempDoc: true, + }; +} 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 new file mode 100644 index 00000000000000..be79f9d7961429 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.test.ts @@ -0,0 +1,103 @@ +/* + * 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 new file mode 100644 index 00000000000000..0fc7bb1b904d94 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/summary_transform_installer.ts @@ -0,0 +1,105 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../assets/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_RESOURCES_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_RESOURCES_VERSION; + const previousTransformAlreadyInstalled = + !!transform && transform._meta?.version !== SLO_RESOURCES_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 new file mode 100644 index 00000000000000..b1df25c43a52bf --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/common.ts @@ -0,0 +1,95 @@ +/* + * 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.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 new file mode 100644 index 00000000000000..68c42db91e923e --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/index.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 { 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 new file mode 100644 index 00000000000000..49a9313f0ca9f3 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_30d_rolling.ts @@ -0,0 +1,153 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + max_page_search_size: 8000, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..b515e4ead626f4 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_7d_rolling.ts @@ -0,0 +1,152 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..3ae7d6c96f054b --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_90d_rolling.ts @@ -0,0 +1,152 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..7a5a8083630cbd --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_monthly_aligned.ts @@ -0,0 +1,150 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..cc897dc84c5072 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_occurrences_weekly_aligned.ts @@ -0,0 +1,150 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..f5dea3efcf8793 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_30d_rolling.ts @@ -0,0 +1,152 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..3bd1e333515296 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_7d_rolling.ts @@ -0,0 +1,152 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..7e9d572d77d157 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_90d_rolling.ts @@ -0,0 +1,152 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..cd7f7e4f36a965 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_monthly_aligned.ts @@ -0,0 +1,180 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_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 new file mode 100644 index 00000000000000..f4808cffac7844 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/summary_transform/templates/summary_timeslices_weekly_aligned.ts @@ -0,0 +1,165 @@ +/* + * 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_RESOURCES_VERSION, + SLO_SUMMARY_DESTINATION_INDEX_NAME, + SLO_SUMMARY_INGEST_PIPELINE_NAME, + SLO_SUMMARY_TRANSFORM_NAME_PREFIX, +} from '../../../../assets/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 { 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, + }, + _meta: { + version: SLO_RESOURCES_VERSION, + managed: true, + managed_by: 'observability', + }, +}; diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap index 5fd17391645edb..d3d0143c3e0753 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 @@ -139,17 +139,453 @@ Object { } `; +exports[`APM Transaction Duration Transform Generator groups by the 'service.environment' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "metric", + ], + }, + }, + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "service.environment": "production", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Duration Transform Generator groups by the 'service.environment' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + +exports[`APM Transaction Duration Transform Generator groups by the 'service.name' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "metric", + ], + }, + }, + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "service.name": "my-service", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Duration Transform Generator groups by the 'service.name' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + +exports[`APM Transaction Duration Transform Generator groups by the 'transaction.name' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "metric", + ], + }, + }, + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "transaction.name": "GET /foo", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Duration Transform Generator groups by the 'transaction.name' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + +exports[`APM Transaction Duration Transform Generator groups by the 'transaction.type' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "metric", + ], + }, + }, + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "transaction.type": "request", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Duration Transform Generator groups by the 'transaction.type' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + exports[`APM Transaction Duration Transform Generator returns the expected transform params for timeslices slo 1`] = ` Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -194,16 +630,86 @@ Object { "fixed_interval": "2m", }, }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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", + }, + }, + "transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, }, }, "settings": Object { @@ -262,18 +768,78 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.apm.transactionDuration')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { @@ -291,12 +857,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -332,16 +898,81 @@ Object { "fixed_interval": "1m", }, }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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", + }, + }, + "transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, }, }, "settings": Object { @@ -400,18 +1031,72 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.apm.transactionDuration')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { 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 eb5ae7388d0e90..bde756ae7ecfa4 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 @@ -131,17 +131,437 @@ Object { } `; +exports[`APM Transaction Error Rate Transform Generator groups by the 'service.environment' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "terms": Object { + "event.outcome": Array [ + "success", + "failure", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "service.environment": "production", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator groups by the 'service.environment' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator groups by the 'service.name' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "terms": Object { + "event.outcome": Array [ + "success", + "failure", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "service.name": "my-service", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator groups by the 'service.name' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.name' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "terms": Object { + "event.outcome": Array [ + "success", + "failure", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "transaction.name": "GET /foo", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.name' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.type' 1`] = ` +Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "terms": Object { + "event.outcome": Array [ + "success", + "failure", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d", + }, + }, + }, + Object { + "match": Object { + "transaction.type": "request", + }, + }, + ], + }, +} +`; + +exports[`APM Transaction Error Rate Transform Generator groups by the 'transaction.type' 2`] = ` +Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, + "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", + }, + }, +} +`; + exports[`APM Transaction Error Rate Transform Generator returns the expected transform params for timeslices slo 1`] = ` Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -179,16 +599,86 @@ Object { "fixed_interval": "2m", }, }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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", + }, + }, + "transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, }, }, "settings": Object { @@ -243,18 +733,78 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.apm.transactionErrorRate')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { @@ -272,12 +822,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -306,16 +856,81 @@ Object { "fixed_interval": "1m", }, }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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", + }, + }, + "transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, }, }, "settings": Object { @@ -370,18 +985,72 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.apm.transactionErrorRate')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { 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 81ad6c77c9564f..2d71f69f9b6b32 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 @@ -64,12 +64,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -138,16 +138,66 @@ Object { "fixed_interval": "2m", }, }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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 { @@ -168,18 +218,78 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.histogram.custom')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { @@ -197,12 +307,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -262,16 +372,61 @@ Object { "fixed_interval": "1m", }, }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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 { @@ -292,18 +447,72 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.histogram.custom')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { 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 011dfd9fe34382..277f8ebf43f78e 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 @@ -105,12 +105,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -153,16 +153,66 @@ Object { "fixed_interval": "2m", }, }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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 { @@ -183,18 +233,78 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.kql.custom')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { @@ -212,12 +322,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -251,16 +361,61 @@ Object { "fixed_interval": "1m", }, }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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 { @@ -281,18 +436,72 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.kql.custom')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { 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 14afc32122d6f8..e94b52cf497a3e 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 @@ -76,12 +76,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -162,16 +162,66 @@ Object { "fixed_interval": "2m", }, }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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 { @@ -192,18 +242,78 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.metric.custom')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { @@ -221,12 +331,12 @@ Object { "_meta": Object { "managed": true, "managed_by": "observability", - "version": 1, + "version": 2, }, "description": "Rolled-up SLI data for SLO: irrelevant", "dest": Object { - "index": ".slo-observability.sli-v1", - "pipeline": ".slo-observability.sli.monthly", + "index": ".slo-observability.sli-v2", + "pipeline": ".slo-observability.sli.pipeline", }, "frequency": "1m", "pivot": Object { @@ -298,16 +408,61 @@ Object { "fixed_interval": "1m", }, }, + "slo.budgetingMethod": Object { + "terms": Object { + "field": "slo.budgetingMethod", + }, + }, + "slo.description": Object { + "terms": Object { + "field": "slo.description", + }, + }, "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 { @@ -328,18 +483,72 @@ 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.id": Object { "script": Object { "source": Any, }, "type": "keyword", }, + "slo.indicator.type": Object { + "script": Object { + "source": "emit('sli.metric.custom')", + }, + "type": "keyword", + }, + "slo.instanceId": Object { + "script": Object { + "source": "emit('*')", + }, + "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 { 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 82a1dd1c65652f..ecbe9abe99af20 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 @@ -15,28 +15,28 @@ import { ApmTransactionDurationTransformGenerator } from './apm_transaction_dura const generator = new ApmTransactionDurationTransformGenerator(); describe('APM Transaction Duration Transform Generator', () => { - it('returns the expected transform params with every specified indicator params', async () => { - const anSLO = createSLO({ indicator: createAPMTransactionDurationIndicator() }); - const transform = generator.getTransformParams(anSLO); + it('returns the expected transform params with every specified indicator params', () => { + const slo = createSLO({ 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-${anSLO.id}-${anSLO.revision}`); + expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`); expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${anSLO.id}')` }, + script: { source: `emit('${slo.id}')` }, }); expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${anSLO.revision})` }, + script: { source: `emit(${slo.revision})` }, }); }); - it('returns the expected transform params for timeslices slo', async () => { - const anSLO = createSLOWithTimeslicesBudgetingMethod({ + it('returns the expected transform params for timeslices slo', () => { + const slo = createSLOWithTimeslicesBudgetingMethod({ indicator: createAPMTransactionDurationIndicator(), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform).toMatchSnapshot({ transform_id: expect.any(String), @@ -44,8 +44,8 @@ describe('APM Transaction Duration Transform Generator', () => { }); }); - it("does not include the query filter when params are '*'", async () => { - const anSLO = createSLO({ + it("does not include the query filter when params are '*'", () => { + const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ environment: '*', service: '*', @@ -53,32 +53,96 @@ describe('APM Transaction Duration Transform Generator', () => { transactionType: '*', }), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); - it('uses the provided index params as source index', async () => { + it('uses the provided index params as source index', () => { const index = 'my-custom-apm-index*'; - const anSLO = createSLO({ + const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ index, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform.source.index).toEqual(index); }); - it('adds the custom kql filter to the query', async () => { + it('adds the custom kql filter to the query', () => { const filter = `"my.field" : "value" and ("foo" >= 12 or "bar" <= 100)`; - const anSLO = createSLO({ + const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ filter, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); + + it("groups by the 'service.name'", () => { + const slo = createSLO({ + indicator: createAPMTransactionDurationIndicator({ + service: 'my-service', + environment: '*', + transactionName: '*', + transactionType: '*', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); + + it("groups by the 'service.environment'", () => { + const slo = createSLO({ + indicator: createAPMTransactionDurationIndicator({ + service: '*', + environment: 'production', + transactionName: '*', + transactionType: '*', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); + + it("groups by the 'transaction.name'", () => { + const slo = createSLO({ + indicator: createAPMTransactionDurationIndicator({ + service: '*', + environment: '*', + transactionName: 'GET /foo', + transactionType: '*', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); + + it("groups by the 'transaction.type'", () => { + const slo = createSLO({ + indicator: createAPMTransactionDurationIndicator({ + service: '*', + environment: '*', + transactionName: '*', + transactionType: 'request', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts index 63ccc7e238398e..3dd6469a7f2c50 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts @@ -11,17 +11,17 @@ import { apmTransactionDurationIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; -import { InvalidTransformError } from '../../../errors'; +import { getElastichsearchQueryOrThrow, TransformGenerator } from '.'; import { + getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, - getSLOTransformId, } from '../../../assets/constants'; import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; -import { SLO, APMTransactionDurationIndicator } from '../../../domain/models'; -import { getElastichsearchQueryOrThrow, TransformGenerator } from '.'; -import { Query } from './types'; +import { APMTransactionDurationIndicator, SLO } from '../../../domain/models'; +import { InvalidTransformError } from '../../../errors'; import { parseIndex } from './common'; +import { Query } from './types'; export class ApmTransactionDurationTransformGenerator extends TransformGenerator { public getTransformParams(slo: SLO): TransformPutTransformRequest { @@ -34,7 +34,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator this.buildDescription(slo), this.buildSource(slo, slo.indicator), this.buildDestination(), - this.buildGroupBy(slo), + this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo, slo.indicator), this.buildSettings(slo) ); @@ -44,6 +44,29 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator return getSLOTransformId(slo.id, slo.revision); } + private buildGroupBy(slo: SLO, indicator: APMTransactionDurationIndicator) { + // These groupBy fields must match the fields from the source query, otherwise + // the transform will create permutations for each value present in the source. + // E.g. if environment is not specified in the source query, but we include it in the groupBy, + // we'll output documents for each environment value + const extraGroupByFields = { + ...(indicator.params.service !== ALL_VALUE && { + 'service.name': { terms: { field: 'service.name' } }, + }), + ...(indicator.params.environment !== ALL_VALUE && { + 'service.environment': { terms: { field: 'service.environment' } }, + }), + ...(indicator.params.transactionName !== ALL_VALUE && { + 'transaction.name': { terms: { field: 'transaction.name' } }, + }), + ...(indicator.params.transactionType !== ALL_VALUE && { + 'transaction.type': { terms: { field: 'transaction.type' } }, + }), + }; + + return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); + } + private buildSource(slo: SLO, indicator: APMTransactionDurationIndicator) { const queryFilter: Query[] = [ { @@ -54,6 +77,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator }, }, ]; + if (indicator.params.service !== ALL_VALUE) { queryFilter.push({ match: { 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 4b19f06e453d0a..338bc0adf3a342 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 @@ -16,27 +16,27 @@ const generator = new ApmTransactionErrorRateTransformGenerator(); describe('APM Transaction Error Rate Transform Generator', () => { it('returns the expected transform params with every specified indicator params', async () => { - const anSLO = createSLO({ indicator: createAPMTransactionErrorRateIndicator() }); - const transform = generator.getTransformParams(anSLO); + const slo = createSLO({ 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-${anSLO.id}-${anSLO.revision}`); + expect(transform.transform_id).toEqual(`slo-${slo.id}-${slo.revision}`); expect(transform.source.runtime_mappings!['slo.id']).toMatchObject({ - script: { source: `emit('${anSLO.id}')` }, + script: { source: `emit('${slo.id}')` }, }); expect(transform.source.runtime_mappings!['slo.revision']).toMatchObject({ - script: { source: `emit(${anSLO.revision})` }, + script: { source: `emit(${slo.revision})` }, }); }); it('returns the expected transform params for timeslices slo', async () => { - const anSLO = createSLOWithTimeslicesBudgetingMethod({ + const slo = createSLOWithTimeslicesBudgetingMethod({ indicator: createAPMTransactionErrorRateIndicator(), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform).toMatchSnapshot({ transform_id: expect.any(String), @@ -45,7 +45,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { }); it("does not include the query filter when params are '*'", async () => { - const anSLO = createSLO({ + const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ environment: '*', service: '*', @@ -53,32 +53,96 @@ describe('APM Transaction Error Rate Transform Generator', () => { transactionType: '*', }), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); it('uses the provided index params as source index', async () => { const index = 'my-custom-apm-index*'; - const anSLO = createSLO({ + const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ index, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform.source.index).toEqual(index); }); it('adds the custom kql filter to the query', async () => { const filter = `"my.field" : "value" and ("foo" >= 12 or "bar" <= 100)`; - const anSLO = createSLO({ + const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ filter, }), }); - const transform = generator.getTransformParams(anSLO); + const transform = generator.getTransformParams(slo); expect(transform.source.query).toMatchSnapshot(); }); + + it("groups by the 'service.name'", () => { + const slo = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ + service: 'my-service', + environment: '*', + transactionName: '*', + transactionType: '*', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); + + it("groups by the 'service.environment'", () => { + const slo = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ + service: '*', + environment: 'production', + transactionName: '*', + transactionType: '*', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); + + it("groups by the 'transaction.name'", () => { + const slo = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ + service: '*', + environment: '*', + transactionName: 'GET /foo', + transactionType: '*', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); + + it("groups by the 'transaction.type'", () => { + const slo = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ + service: '*', + environment: '*', + transactionName: '*', + transactionType: 'request', + }), + }); + + const transform = generator.getTransformParams(slo); + + expect(transform.source.query).toMatchSnapshot(); + expect(transform.pivot?.group_by).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts index d2ba9114360962..3818111c70df21 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts @@ -11,18 +11,17 @@ import { apmTransactionErrorRateIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; - -import { InvalidTransformError } from '../../../errors'; -import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; import { getElastichsearchQueryOrThrow, TransformGenerator } from '.'; import { + getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, - getSLOTransformId, } from '../../../assets/constants'; +import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; import { APMTransactionErrorRateIndicator, SLO } from '../../../domain/models'; -import { Query } from './types'; +import { InvalidTransformError } from '../../../errors'; import { parseIndex } from './common'; +import { Query } from './types'; export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator { public getTransformParams(slo: SLO): TransformPutTransformRequest { @@ -35,7 +34,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato this.buildDescription(slo), this.buildSource(slo, slo.indicator), this.buildDestination(), - this.buildGroupBy(slo), + this.buildGroupBy(slo, slo.indicator), this.buildAggregations(slo), this.buildSettings(slo) ); @@ -45,6 +44,29 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato return getSLOTransformId(slo.id, slo.revision); } + private buildGroupBy(slo: SLO, indicator: APMTransactionErrorRateIndicator) { + // These groupBy fields must match the fields from the source query, otherwise + // the transform will create permutations for each value present in the source. + // E.g. if environment is not specified in the source query, but we include it in the groupBy, + // we'll output documents for each environment value + const extraGroupByFields = { + ...(indicator.params.service !== ALL_VALUE && { + 'service.name': { terms: { field: 'service.name' } }, + }), + ...(indicator.params.environment !== ALL_VALUE && { + 'service.environment': { terms: { field: 'service.environment' } }, + }), + ...(indicator.params.transactionName !== ALL_VALUE && { + 'transaction.name': { terms: { field: 'transaction.name' } }, + }), + ...(indicator.params.transactionType !== ALL_VALUE && { + 'transaction.type': { terms: { field: 'transaction.type' } }, + }), + }; + + return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); + } + private buildSource(slo: SLO, indicator: APMTransactionErrorRateIndicator) { const queryFilter: Query[] = [ { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.ts index f120d27d98a8a5..7793123ff1595c 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/histogram.ts @@ -34,7 +34,7 @@ export class HistogramTransformGenerator extends TransformGenerator { this.buildDescription(slo), this.buildSource(slo, slo.indicator), this.buildDestination(), - this.buildGroupBy(slo, slo.indicator.params.timestampField), + this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), this.buildSettings(slo, slo.indicator.params.timestampField) ); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts index 49151c158952af..e44bab97e1b78d 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts @@ -29,7 +29,7 @@ export class KQLCustomTransformGenerator extends TransformGenerator { this.buildDescription(slo), this.buildSource(slo, slo.indicator), this.buildDestination(), - this.buildGroupBy(slo, slo.indicator.params.timestampField), + this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), this.buildSettings(slo, slo.indicator.params.timestampField) ); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts index 16a83de0cb3e8d..8faa996272e466 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts @@ -32,7 +32,7 @@ export class MetricCustomTransformGenerator extends TransformGenerator { this.buildDescription(slo), this.buildSource(slo, slo.indicator), this.buildDestination(), - this.buildGroupBy(slo, slo.indicator.params.timestampField), + this.buildCommonGroupBy(slo, slo.indicator.params.timestampField), this.buildAggregations(slo, slo.indicator), this.buildSettings(slo, slo.indicator.params.timestampField) ); 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 fcbb45cebe18a8..57194acb8d46cc 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 @@ -5,30 +5,93 @@ * 2.0. */ -import { MappingRuntimeFieldType } from '@elastic/elasticsearch/lib/api/types'; -import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; - +import { + MappingRuntimeFields, + TransformPutTransformRequest, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ALL_VALUE, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; import { TransformSettings } from '../../../assets/transform_templates/slo_transform_template'; import { SLO } from '../../../domain/models'; export abstract class TransformGenerator { public abstract getTransformParams(slo: SLO): TransformPutTransformRequest; - public buildCommonRuntimeMappings(slo: SLO) { + public buildCommonRuntimeMappings(slo: SLO): MappingRuntimeFields { return { 'slo.id': { - type: 'keyword' as MappingRuntimeFieldType, + type: 'keyword', script: { source: `emit('${slo.id}')`, }, }, 'slo.revision': { - type: 'long' as MappingRuntimeFieldType, + type: 'long', script: { source: `emit(${slo.revision})`, }, }, + 'slo.instanceId': { + type: 'keyword', + script: { + source: `emit('${ALL_VALUE}')`, + }, + }, + '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}')`, + }, + }, }; } @@ -36,24 +99,35 @@ export abstract class TransformGenerator { return `Rolled-up SLI data for SLO: ${slo.name}`; } - public buildGroupBy(slo: SLO, sourceIndexTimestampField: string | undefined = '@timestamp') { + public buildCommonGroupBy( + slo: SLO, + sourceIndexTimestampField: string | undefined = '@timestamp', + extraGroupByFields = {} + ) { let fixedInterval = '1m'; if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) { fixedInterval = slo.objective.timesliceWindow!.format(); } return { - 'slo.id': { - terms: { - field: 'slo.id', + 'slo.id': { terms: { field: 'slo.id' } }, + 'slo.revision': { terms: { field: 'slo.revision' } }, + '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.objective.target': { terms: { field: 'slo.objective.target' } }, + ...(slo.objective.timesliceWindow && { + 'slo.objective.sliceDurationInSeconds': { + terms: { field: 'slo.objective.sliceDurationInSeconds' }, }, - }, - 'slo.revision': { - terms: { - field: 'slo.revision', - }, - }, - // timestamp field defined in the destination index + }), + 'slo.budgetingMethod': { terms: { field: 'slo.budgetingMethod' } }, + 'slo.timeWindow.duration': { terms: { field: 'slo.timeWindow.duration' } }, + 'slo.timeWindow.type': { terms: { field: 'slo.timeWindow.type' } }, + ...extraGroupByFields, + // @timestamp field defined in the destination index '@timestamp': { date_histogram: { field: sourceIndexTimestampField, // timestamp field defined in the source index 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 17fc582dee1605..d2e03fe97dde82 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 @@ -8,7 +8,11 @@ import { ElasticsearchClient } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; -import { getSLOTransformId } from '../../assets/constants'; +import { + getSLOTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, +} from '../../assets/constants'; import { SLO } from '../../domain/models'; import { fiveMinute, oneMinute } from './fixtures/duration'; import { @@ -34,131 +38,107 @@ describe('UpdateSLO', () => { updateSLO = new UpdateSLO(mockRepository, mockTransformManager, mockEsClient); }); - describe('without breaking changes', () => { - it('updates the SLO saved object without revision bump', async () => { - const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() }); - mockRepository.findById.mockResolvedValueOnce(slo); - - const newName = 'new slo name'; - const newTags = ['other', 'tags']; - const response = await updateSLO.execute(slo.id, { name: newName, tags: newTags }); - - expectTransformManagerNeverCalled(); - expect(mockEsClient.deleteByQuery).not.toBeCalled(); - expect(mockRepository.save).toBeCalledWith( - expect.objectContaining({ - ...slo, - name: newName, - tags: newTags, - updatedAt: expect.anything(), - }) - ); - expect(slo.name).not.toEqual(newName); - expect(response.name).toEqual(newName); - expect(response.updatedAt).not.toEqual(slo.updatedAt); - expect(response.revision).toEqual(slo.revision); - expect(response.tags).toEqual(newTags); - expect(slo.tags).not.toEqual(newTags); - }); + it('updates the settings correctly', async () => { + const slo = createSLO(); + mockRepository.findById.mockResolvedValueOnce(slo); + + const newSettings = { ...slo.settings, timestamp_field: 'newField' }; + await updateSLO.execute(slo.id, { settings: newSettings }); + + expectDeletionOfObsoleteSLOData(slo); + expect(mockRepository.save).toBeCalledWith( + expect.objectContaining({ + ...slo, + settings: newSettings, + revision: 2, + updatedAt: expect.anything(), + }) + ); + expectInstallationOfNewSLOTransform(); }); - describe('with breaking changes', () => { - it('consideres settings 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 }); - - expectDeletionOfObsoleteSLOData(slo); - expect(mockRepository.save).toBeCalledWith( - expect.objectContaining({ - ...slo, - settings: newSettings, - revision: 2, - updatedAt: expect.anything(), - }) - ); - expectInstallationOfNewSLOTransform(); + 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(), + }, }); - it('consideres a budgeting method change as a breaking change', async () => { - const slo = createSLO({ budgetingMethod: 'occurrences' }); - mockRepository.findById.mockResolvedValueOnce(slo); + expectDeletionOfObsoleteSLOData(slo); + expectInstallationOfNewSLOTransform(); + }); - await updateSLO.execute(slo.id, { - budgetingMethod: 'timeslices', - objective: { - target: slo.objective.target, - timesliceTarget: 0.9, - timesliceWindow: oneMinute(), - }, - }); + it('updates the timeslice target correctly', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod(); + mockRepository.findById.mockResolvedValueOnce(slo); - expectDeletionOfObsoleteSLOData(slo); - expectInstallationOfNewSLOTransform(); + await updateSLO.execute(slo.id, { + objective: { + target: slo.objective.target, + timesliceTarget: 0.1, + timesliceWindow: slo.objective.timesliceWindow, + }, }); - it('consideres a timeslice target change as a breaking change', async () => { - const slo = createSLOWithTimeslicesBudgetingMethod(); - mockRepository.findById.mockResolvedValueOnce(slo); + expectDeletionOfObsoleteSLOData(slo); + expectInstallationOfNewSLOTransform(); + }); - await updateSLO.execute(slo.id, { - objective: { - target: slo.objective.target, - timesliceTarget: 0.1, - timesliceWindow: slo.objective.timesliceWindow, - }, - }); + it('consideres a timeslice window change as a breaking change', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod(); + mockRepository.findById.mockResolvedValueOnce(slo); - expectDeletionOfObsoleteSLOData(slo); - expectInstallationOfNewSLOTransform(); + await updateSLO.execute(slo.id, { + objective: { + target: slo.objective.target, + timesliceTarget: slo.objective.timesliceTarget, + timesliceWindow: fiveMinute(), + }, }); - 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: slo.objective.timesliceTarget, - timesliceWindow: fiveMinute(), - }, - }); + expectDeletionOfObsoleteSLOData(slo); + expectInstallationOfNewSLOTransform(); + }); - expectDeletionOfObsoleteSLOData(slo); - expectInstallationOfNewSLOTransform(); + it('index a temporary summary document', async () => { + const slo = createSLO({ + id: 'unique-id', + indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), }); + mockRepository.findById.mockResolvedValueOnce(slo); - it('removes the obsolete data from the SLO previous revision', async () => { - const slo = createSLO({ - indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), - }); - mockRepository.findById.mockResolvedValueOnce(slo); - - const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); - await updateSLO.execute(slo.id, { indicator: newIndicator }); - - expectDeletionOfObsoleteSLOData(slo); - expect(mockRepository.save).toBeCalledWith( - expect.objectContaining({ - ...slo, - indicator: newIndicator, - revision: 2, - updatedAt: expect.anything(), - }) - ); - expectInstallationOfNewSLOTransform(); - }); + const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); + await updateSLO.execute(slo.id, { indicator: newIndicator }); + + expect(mockEsClient.index.mock.calls[0]).toMatchSnapshot(); }); - function expectTransformManagerNeverCalled() { - expect(mockTransformManager.stop).not.toBeCalled(); - expect(mockTransformManager.uninstall).not.toBeCalled(); - expect(mockTransformManager.start).not.toBeCalled(); - expect(mockTransformManager.install).not.toBeCalled(); - } + it('removes the obsolete data from the SLO previous revision', async () => { + const slo = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), + }); + mockRepository.findById.mockResolvedValueOnce(slo); + + const newIndicator = createAPMTransactionErrorRateIndicator({ environment: 'production' }); + await updateSLO.execute(slo.id, { indicator: newIndicator }); + + expectDeletionOfObsoleteSLOData(slo); + expect(mockRepository.save).toBeCalledWith( + expect.objectContaining({ + ...slo, + indicator: newIndicator, + revision: 2, + updatedAt: expect.anything(), + }) + ); + expectInstallationOfNewSLOTransform(); + }); function expectInstallationOfNewSLOTransform() { expect(mockTransformManager.start).toBeCalled(); @@ -169,8 +149,26 @@ describe('UpdateSLO', () => { const transformId = getSLOTransformId(originalSlo.id, originalSlo.revision); expect(mockTransformManager.stop).toBeCalledWith(transformId); expect(mockTransformManager.uninstall).toBeCalledWith(transformId); - expect(mockEsClient.deleteByQuery).toBeCalledWith( + expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2); + + expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + index: SLO_DESTINATION_INDEX_PATTERN, + query: { + bool: { + filter: [ + { term: { 'slo.id': originalSlo.id } }, + { term: { 'slo.revision': originalSlo.revision } }, + ], + }, + }, + }) + ); + expect(mockEsClient.deleteByQuery).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, query: { bool: { filter: [ 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 cf2c0e2e1c89fa..f89735df501f4c 100644 --- a/x-pack/plugins/observability/server/services/slo/update_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/update_slo.ts @@ -5,15 +5,19 @@ * 2.0. */ -import deepEqual from 'fast-deep-equal'; import { ElasticsearchClient } from '@kbn/core/server'; import { UpdateSLOParams, UpdateSLOResponse, updateSLOResponseSchema } from '@kbn/slo-schema'; - -import { getSLOTransformId, SLO_INDEX_TEMPLATE_NAME } from '../../assets/constants'; -import { SLORepository } from './slo_repository'; -import { TransformManager } from './transform_manager'; +import { + getSLOTransformId, + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_TEMP_INDEX_NAME, +} from '../../assets/constants'; import { SLO } from '../../domain/models'; import { validateSLO } from '../../domain/services'; +import { SLORepository } from './slo_repository'; +import { createTempSummaryDocument } from './summary_transform/helpers/create_temp_summary'; +import { TransformManager } from './transform_manager'; export class UpdateSLO { constructor( @@ -24,52 +28,25 @@ export class UpdateSLO { public async execute(sloId: string, params: UpdateSLOParams): Promise { const originalSlo = await this.repository.findById(sloId); - const { hasBreakingChange, updatedSlo } = this.updateSLO(originalSlo, params); - - if (hasBreakingChange) { - await this.deleteObsoleteSLORevisionData(originalSlo); - - await this.repository.save(updatedSlo); - await this.transformManager.install(updatedSlo); - await this.transformManager.start(getSLOTransformId(updatedSlo.id, updatedSlo.revision)); - } else { - await this.repository.save(updatedSlo); - } - - return this.toResponse(updatedSlo); - } + const updatedSlo: SLO = Object.assign({}, originalSlo, params, { + updatedAt: new Date(), + revision: originalSlo.revision + 1, + }); - private updateSLO(originalSlo: SLO, params: UpdateSLOParams) { - let hasBreakingChange = false; - const updatedSlo: SLO = Object.assign({}, originalSlo, params, { updatedAt: new Date() }); validateSLO(updatedSlo); - if (!deepEqual(originalSlo.indicator, updatedSlo.indicator)) { - hasBreakingChange = true; - } - - if (originalSlo.budgetingMethod !== updatedSlo.budgetingMethod) { - hasBreakingChange = true; - } - - if ( - originalSlo.budgetingMethod === 'timeslices' && - updatedSlo.budgetingMethod === 'timeslices' && - (originalSlo.objective.timesliceTarget !== updatedSlo.objective.timesliceTarget || - !deepEqual(originalSlo.objective.timesliceWindow, updatedSlo.objective.timesliceWindow)) - ) { - hasBreakingChange = true; - } + await this.deleteObsoleteSLORevisionData(originalSlo); + await this.repository.save(updatedSlo); + await this.transformManager.install(updatedSlo); + await this.transformManager.start(getSLOTransformId(updatedSlo.id, updatedSlo.revision)); - if (!deepEqual(originalSlo.settings, updatedSlo.settings)) { - hasBreakingChange = true; - } - - if (hasBreakingChange) { - updatedSlo.revision++; - } + await this.esClient.index({ + index: SLO_SUMMARY_TEMP_INDEX_NAME, + id: `slo-${updatedSlo.id}`, + document: createTempSummaryDocument(updatedSlo), + }); - return { hasBreakingChange, updatedSlo }; + return this.toResponse(updatedSlo); } private async deleteObsoleteSLORevisionData(originalSlo: SLO) { @@ -77,11 +54,24 @@ export class UpdateSLO { await this.transformManager.stop(originalSloTransformId); await this.transformManager.uninstall(originalSloTransformId); await this.deleteRollupData(originalSlo.id, originalSlo.revision); + await this.deleteSummaryData(originalSlo.id, originalSlo.revision); } private async deleteRollupData(sloId: string, sloRevision: number): Promise { await this.esClient.deleteByQuery({ - index: `${SLO_INDEX_TEMPLATE_NAME}*`, + index: SLO_DESTINATION_INDEX_PATTERN, + wait_for_completion: false, + query: { + bool: { + filter: [{ term: { 'slo.id': sloId } }, { term: { 'slo.revision': sloRevision } }], + }, + }, + }); + } + + private async deleteSummaryData(sloId: string, sloRevision: number): Promise { + await this.esClient.deleteByQuery({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, wait_for_completion: false, query: { bool: { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2a302d6c4684ef..45101f9bc6ed8d 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27313,7 +27313,6 @@ "xpack.observability.slo.duration.minute": "{duration, plural, one {1 minute} many {# minutes} other {# minutes}}", "xpack.observability.slo.duration.month": "{duration, plural, one {1 mois} many {# mois} other {# mois}}", "xpack.observability.slo.duration.week": "{duration, plural, one {1 semaine} many {# semaines} other {# semaines}}", - "xpack.observability.slo.duration.year": "{duration, plural, one {1 an} many {# ans} other {# prochaines années}}", "xpack.observability.slo.indicatorTypeBadge.exploreInApm": "Afficher les détails de {service}", "xpack.observability.slo.list.sortByType": "Trier par {type}", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "Le seuil du taux d'avancement doit être compris entre 1 et {maxBurnRate}.", @@ -27612,7 +27611,6 @@ "xpack.observability.slo.duration.minutely": "Par minute", "xpack.observability.slo.duration.monthly": "Mensuel", "xpack.observability.slo.duration.weekly": "Hebdomadaire", - "xpack.observability.slo.duration.yearly": "Annuel", "xpack.observability.slo.feedbackButtonLabel": "Dites-nous ce que vous pensez !", "xpack.observability.slo.globalDiagnosis.errorNotification": "Vous ne disposez pas des autorisations nécessaires pour utiliser cette fonctionnalité.", "xpack.observability.slo.indicators.apmAvailability": "Disponibilité APM", @@ -27624,11 +27622,8 @@ "xpack.observability.slo.list.errorMessage": "Une erreur s'est produite lors du chargement des SLO. Contactez votre administrateur pour obtenir de l'aide.", "xpack.observability.slo.list.errorNotification": "Un problème est survenu lors de la récupération des SLO", "xpack.observability.slo.list.errorTitle": "Impossible de charger les SLO", - "xpack.observability.slo.list.indicatorTypeFilter": "Type d’indicateur", "xpack.observability.slo.list.search": "Recherche", "xpack.observability.slo.list.sortBy": "Trier par", - "xpack.observability.slo.list.sortBy.creationTime": "Heure de création", - "xpack.observability.slo.list.sortBy.indicatorType": "Type d’indicateur", "xpack.observability.slo.rules.actionGroupSelectorLabel": "Groupe d’action", "xpack.observability.slo.rules.addWindowAriaLabel": "Ajouter une fenêtre", "xpack.observability.slo.rules.addWIndowLabel": "Ajouter une fenêtre", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 073ea3cfba4ff9..b3503b6020b495 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27313,7 +27313,6 @@ "xpack.observability.slo.duration.minute": "{duration, plural, other {#分}}", "xpack.observability.slo.duration.month": "{duration, plural, other {#月}}", "xpack.observability.slo.duration.week": "{duration, plural, other {#週}}", - "xpack.observability.slo.duration.year": "{duration, plural, other {#年}}", "xpack.observability.slo.indicatorTypeBadge.exploreInApm": "{service}詳細を表示", "xpack.observability.slo.list.sortByType": "{type}で並べ替え", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "バーンレートしきい値は1以上{maxBurnRate}以下でなければなりません。", @@ -27612,7 +27611,6 @@ "xpack.observability.slo.duration.minutely": "毎分", "xpack.observability.slo.duration.monthly": "月ごと", "xpack.observability.slo.duration.weekly": "週ごと", - "xpack.observability.slo.duration.yearly": "年ごと", "xpack.observability.slo.feedbackButtonLabel": "ご意見をお聞かせください。", "xpack.observability.slo.globalDiagnosis.errorNotification": "この機能を使用する権限がありません。", "xpack.observability.slo.indicators.apmAvailability": "APM可用性", @@ -27624,11 +27622,9 @@ "xpack.observability.slo.list.errorMessage": "SLOオブジェクトの読み込みエラーが発生しました。ヘルプについては、管理者にお問い合わせください。", "xpack.observability.slo.list.errorNotification": "SLOの取得中に問題が発生しました", "xpack.observability.slo.list.errorTitle": "SLOを読み込めません", - "xpack.observability.slo.list.indicatorTypeFilter": "インジケータータイプ", "xpack.observability.slo.list.search": "検索", "xpack.observability.slo.list.sortBy": "並べ替え基準", - "xpack.observability.slo.list.sortBy.creationTime": "作成時刻", - "xpack.observability.slo.list.sortBy.indicatorType": "インジケータータイプ", + "xpack.observability.slo.rules.actionGroupSelectorLabel": "アクショングループ", "xpack.observability.slo.rules.addWindowAriaLabel": "時間枠を追加", "xpack.observability.slo.rules.addWIndowLabel": "時間枠を追加", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 290da349a0e634..5c725f1b1571b8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27311,7 +27311,6 @@ "xpack.observability.slo.duration.minute": "{duration, plural, other {# 分钟}}", "xpack.observability.slo.duration.month": "{duration, plural, other {# 个月}}", "xpack.observability.slo.duration.week": "{duration, plural, other {# 周}}", - "xpack.observability.slo.duration.year": "{duration, plural, other {# 年}}", "xpack.observability.slo.indicatorTypeBadge.exploreInApm": "查看 {service} 详情", "xpack.observability.slo.list.sortByType": "按 {type} 排序", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "消耗速度阈值必须介于 1 和 {maxBurnRate} 之间。", @@ -27610,7 +27609,6 @@ "xpack.observability.slo.duration.minutely": "每分钟", "xpack.observability.slo.duration.monthly": "每月", "xpack.observability.slo.duration.weekly": "每周", - "xpack.observability.slo.duration.yearly": "每年", "xpack.observability.slo.feedbackButtonLabel": "告诉我们您的看法!", "xpack.observability.slo.globalDiagnosis.errorNotification": "您没有适当权限,无法使用此功能。", "xpack.observability.slo.indicators.apmAvailability": "APM 可用性", @@ -27622,11 +27620,8 @@ "xpack.observability.slo.list.errorMessage": "加载 SLO 时出错。请联系您的管理员寻求帮助。", "xpack.observability.slo.list.errorNotification": "提取 SLO 时出现问题", "xpack.observability.slo.list.errorTitle": "无法加载 SLO", - "xpack.observability.slo.list.indicatorTypeFilter": "指标类型", "xpack.observability.slo.list.search": "搜索", "xpack.observability.slo.list.sortBy": "排序依据", - "xpack.observability.slo.list.sortBy.creationTime": "创建时间", - "xpack.observability.slo.list.sortBy.indicatorType": "指标类型", "xpack.observability.slo.rules.actionGroupSelectorLabel": "操作组", "xpack.observability.slo.rules.addWindowAriaLabel": "添加窗口", "xpack.observability.slo.rules.addWIndowLabel": "添加窗口",