From 721d354a13f52a88945edfa485c719ba9df98d5a Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 16 Apr 2024 09:15:51 -0400 Subject: [PATCH] [SLO] Implement federated views (#178050) --- .../src/constants.ts | 1 + .../current_fields.json | 1 + .../current_mappings.json | 4 + .../src/decode_request_params.ts | 4 +- .../kbn-server-route-repository/tsconfig.json | 3 +- .../check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 1 + .../group5/dot_kibana_split.test.ts | 1 + x-pack/packages/kbn-slo-schema/index.ts | 3 +- .../kbn-slo-schema/src/models/index.ts} | 3 +- .../kbn-slo-schema/src/rest_specs/common.ts | 21 + .../kbn-slo-schema/src/rest_specs/index.ts | 3 + .../src/rest_specs/indicators.ts | 60 +++ .../src/rest_specs/routes/create.ts | 47 ++ .../src/rest_specs/routes/delete.ts} | 15 +- .../src/rest_specs/routes/delete_instance.ts | 19 + .../routes/fetch_historical_summary.ts | 68 +++ .../src/rest_specs/routes/find.ts | 40 ++ .../src/rest_specs/routes/find_definition.ts | 31 ++ .../src/rest_specs/routes/find_group.ts | 50 +++ .../src/rest_specs/routes/get.ts | 34 ++ .../src/rest_specs/routes/get_burn_rates.ts | 40 ++ .../src/rest_specs/routes/get_instances.ts | 21 + .../src/rest_specs/routes/get_preview_data.ts | 50 +++ .../src/rest_specs/routes/index.ts | 22 + .../src/rest_specs/routes/manage.ts | 17 + .../src/rest_specs/routes/put_settings.ts | 21 + .../src/rest_specs/routes/reset.ts | 20 + .../src/rest_specs/routes/update.ts | 43 ++ .../kbn-slo-schema/src/rest_specs/slo.ts | 423 +----------------- .../kbn-slo-schema/src/schema/common.ts | 61 +-- .../kbn-slo-schema/src/schema/index.ts | 1 + .../kbn-slo-schema/src/schema/indicators.ts | 34 +- .../kbn-slo-schema/src/schema/settings.ts} | 8 +- .../packages/kbn-slo-schema/src/schema/slo.ts | 17 +- .../register_apm_server_routes.test.ts | 3 +- .../observability/common/locators/paths.ts | 1 + .../server/domain/models/common.ts | 27 -- .../server/domain/models/index.ts | 13 - .../server/domain/models/indicators.ts | 38 -- .../observability/server/domain/models/slo.ts | 16 - .../server/domain/models/time_window.ts | 46 -- .../domain/services/compute_burn_rate.ts | 24 - .../domain/services/compute_sli.test.ts | 26 -- .../server/domain/services/compute_sli.ts | 18 - .../domain/services/compute_summary_status.ts | 20 - .../server/domain/services/date_range.ts | 40 -- .../server/domain/services/error_budget.ts | 22 - .../services/get_delay_in_seconds_from_slo.ts | 18 - .../services/get_lookback_date_range.ts | 23 - .../server/domain/services/validate_slo.ts | 122 ----- .../observability/server/utils/queries.ts | 4 + .../slo/common/locators/paths.ts | 18 +- .../slo/common/summary_indices.test.ts | 50 +++ .../slo/common/summary_indices.ts | 28 ++ .../alert_time_table.tsx | 8 +- .../burn_rate_rule_editor.tsx | 8 +- .../slo_selector.stories.tsx | 9 +- .../burn_rate_rule_editor/slo_selector.tsx | 6 +- .../burn_rate_rule_editor/windows.tsx | 10 +- .../components/header_menu/header_menu.tsx | 10 + .../components/slo/burn_rate/burn_rate.tsx | 6 +- .../slo_delete_confirmation_modal.tsx | 4 +- .../slo/error_rate_chart/error_rate_chart.tsx | 4 +- .../error_rate_chart/use_lens_definition.ts | 8 +- .../slo_reset_confirmation_modal.tsx | 4 +- .../slo/slo_status_badge/slo_status_badge.tsx | 36 +- .../slo_error_budget_burn_down.tsx | 9 +- .../slo/overview/slo_configuration.tsx | 9 +- .../slo/overview/slo_embeddable.tsx | 11 +- .../embeddable/slo/overview/slo_overview.tsx | 4 +- .../slo/overview/slo_overview_grid.tsx | 7 +- .../public/embeddable/slo/overview/types.ts | 4 +- .../use_fetch_historical_summary.ts | 8 +- .../slo/public/hooks/active_alerts.ts | 4 +- .../slo/public/hooks/use_clone_slo.ts | 24 +- .../slo/public/hooks/use_create_data_view.ts | 2 +- .../hooks/use_fetch_historical_summary.ts | 17 +- .../public/hooks/use_fetch_slo_burn_rates.ts | 6 +- .../slo/public/hooks/use_fetch_slo_details.ts | 3 + .../slo/public/hooks/use_fetch_slo_inspect.ts | 4 +- .../slo/public/hooks/use_get_preview_data.ts | 3 + .../slo/public/hooks/use_space.ts | 22 + .../components/events_chart_panel.tsx | 1 + .../slo_details/components/header_control.tsx | 127 ++++-- .../slo_details/components/header_title.tsx | 5 +- .../slo_details/components/slo_details.tsx | 5 +- .../components/slo_remote_callout.tsx | 62 +++ .../hooks/use_get_instance_id_query_param.ts | 20 - .../slo_details/hooks/use_get_query_params.ts | 44 ++ .../slo_details/hooks/use_slo_actions.ts | 90 ++++ .../hooks/use_slo_details_tabs.tsx | 37 +- .../public/pages/slo_details/slo_details.tsx | 5 +- .../slo_edit_form_objective_section.tsx | 4 +- .../slo/public/pages/slo_edit/constants.ts | 4 +- .../slo/public/pages/slo_edit/types.ts | 4 +- .../slo_outdated_definitions/outdated_slo.tsx | 14 +- .../pages/slo_settings/settings_form.tsx | 162 +++++++ .../pages/slo_settings/slo_settings.tsx | 45 ++ .../pages/slo_settings/use_get_settings.ts | 33 ++ .../slo_settings/use_put_slo_settings.tsx | 48 ++ .../slos/components/badges/slo_badges.tsx | 4 +- .../badges/slo_indicator_type_badge.tsx | 51 +-- .../components/badges/slo_remote_badge.tsx | 40 ++ .../components/badges/slo_rules_badge.tsx | 7 +- .../badges/slo_time_window_badge.tsx | 10 +- .../components/card_view/slo_card_item.tsx | 1 - .../card_view/slo_card_item_badges.tsx | 5 +- .../components/card_view/slos_card_view.tsx | 2 +- .../slos/components/common/quick_filters.tsx | 15 +- .../slos/components/common/sort_by_select.tsx | 3 +- .../compact_view/slo_list_compact_view.tsx | 164 ++++--- .../grouped_slos/group_list_view.tsx | 5 +- .../components/grouped_slos/group_view.tsx | 2 +- .../slos/components/slo_item_actions.tsx | 111 +++-- .../slos/components/slo_list_group_by.tsx | 21 +- .../slos/components/slo_list_search_bar.tsx | 31 +- .../slo_list_item.stories.tsx | 4 +- .../{ => slo_list_view}/slo_list_item.tsx | 48 +- .../slo_list_view/slo_list_view.tsx | 4 +- .../pages/slos/components/slos_view.tsx | 28 +- .../pages/slos/hooks/use_slo_list_actions.ts | 5 +- .../pages/slos/hooks/use_slo_summary.ts | 3 +- .../pages/slos/hooks/use_summary_dataview.ts | 39 ++ .../pages/slos/hooks/use_url_search_state.ts | 3 +- .../slo/public/pages/slos/slos.test.tsx | 10 + .../slo/public/routes/routes.tsx | 9 + ..._sli_apm_params_to_apm_app_deeplink_url.ts | 13 +- .../slo/get_delay_in_seconds_from_slo.ts | 8 +- .../slo/public/utils/slo/get_discover_link.ts | 4 +- .../public/utils/slo/remote_slo_urls.test.ts | 77 ++++ .../slo/public/utils/slo/remote_slo_urls.ts | 71 +++ .../slo_summary_pipeline_template.ts | 6 +- .../slo_transform_template.ts | 8 +- .../slo/server/domain/models/common.ts | 25 +- .../slo/server/domain/models/slo.ts | 10 +- .../domain/services/compute_burn_rate.ts | 4 +- .../services/compute_summary_status.test.ts | 15 +- .../domain/services/compute_summary_status.ts | 10 +- .../services/get_delay_in_seconds_from_slo.ts | 4 +- .../server/domain/services/validate_slo.ts | 10 +- .../slo/server/lib/collectors/fetcher.ts | 4 +- .../lib/rules/slo_burn_rate/executor.test.ts | 54 +-- .../lib/rules/slo_burn_rate/fixtures/rule.ts | 4 +- .../rules/slo_burn_rate/lib/build_query.ts | 8 +- .../lib/rules/slo_burn_rate/lib/evaluate.ts | 6 +- .../lib/evaluate_dependencies.ts | 4 +- .../lib/should_suppress_instance_id.test.ts | 8 +- .../slo/server/plugin.ts | 2 + .../slo/server/routes/slo/route.ts | 93 ++-- .../slo/server/saved_objects/slo.ts | 9 +- .../slo/server/saved_objects/slo_settings.ts | 37 ++ .../__snapshots__/create_slo.test.ts.snap | 16 +- .../__snapshots__/reset_slo.test.ts.snap | 17 +- .../slo_definition_client.test.ts.snap | 59 +++ .../summary_search_client.test.ts.snap | 10 +- .../slo/server/services/create_slo.ts | 12 +- .../services/fetch_historical_summary.ts | 21 +- .../slo/server/services/find_slo.test.ts | 8 +- .../slo/server/services/find_slo.ts | 58 ++- .../slo/server/services/find_slo_groups.ts | 17 +- .../slo/server/services/fixtures/slo.ts | 22 +- .../slo/server/services/get_burn_rates.ts | 28 +- .../slo/server/services/get_preview_data.ts | 44 +- .../slo/server/services/get_slo.test.ts | 13 +- .../slo/server/services/get_slo.ts | 46 +- .../historical_summary_client.test.ts | 70 ++- .../services/historical_summary_client.ts | 155 ++++--- .../slo/server/services/reset_slo.ts | 2 +- .../slo/server/services/sli_client.ts | 19 +- .../services/slo_definition_client.test.ts | 94 ++++ .../server/services/slo_definition_client.ts | 65 +++ .../server/services/slo_repository.test.ts | 34 +- .../slo/server/services/slo_repository.ts | 44 +- .../slo/server/services/slo_settings.ts | 71 +++ .../server/services/summary_client.test.ts | 8 +- .../slo/server/services/summary_client.ts | 56 ++- .../services/summary_search_client.test.ts | 16 +- .../server/services/summary_search_client.ts | 196 ++++---- .../generators/common.ts | 4 +- .../generators/occurrences.ts | 6 +- .../generators/timeslices_calendar_aligned.ts | 4 +- .../generators/timeslices_rolling.ts | 4 +- .../helpers/create_temp_summary.ts | 71 ++- .../summary_transform_generator.ts | 8 +- .../services/summay_transform_manager.ts | 9 +- .../tasks/orphan_summary_cleanup_task.ts | 4 +- .../apm_transaction_duration.ts | 16 +- .../apm_transaction_error_rate.ts | 12 +- .../transform_generators/histogram.ts | 17 +- .../transform_generators/kql_custom.ts | 10 +- .../transform_generators/metric_custom.ts | 17 +- .../synthetics_availability.test.ts | 8 +- .../synthetics_availability.ts | 16 +- .../transform_generators/timeslice_metric.ts | 10 +- .../transform_generator.ts | 15 +- .../server/services/transform_manager.test.ts | 6 +- .../slo/server/services/transform_manager.ts | 10 +- .../remote_summary_doc_to_slo.test.ts.snap | 102 +++++ .../remote_summary_doc_to_slo.test.ts | 133 ++++++ .../remote_summary_doc_to_slo.ts | 171 +++++++ .../slo/server/services/update_slo.test.ts | 4 +- .../slo/server/services/update_slo.ts | 10 +- .../slo/server/services/utils/index.ts | 2 +- .../slo/server/utils/queries.ts | 4 + .../observability_solution/slo/tsconfig.json | 7 +- 206 files changed, 3808 insertions(+), 1897 deletions(-) rename x-pack/{plugins/observability_solution/slo/public/pages/slo_details/hooks/use_error_budget_actions.ts => packages/kbn-slo-schema/src/models/index.ts} (81%) create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/common.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/indicators.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/create.ts rename x-pack/{plugins/observability_solution/observability/server/domain/services/index.ts => packages/kbn-slo-schema/src/rest_specs/routes/delete.ts} (55%) create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/delete_instance.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_definition.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_burn_rates.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_instances.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_preview_data.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/index.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/manage.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/put_settings.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/reset.ts create mode 100644 x-pack/packages/kbn-slo-schema/src/rest_specs/routes/update.ts rename x-pack/{plugins/observability_solution/observability/server/domain/models/error_budget.ts => packages/kbn-slo-schema/src/schema/settings.ts} (67%) delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/models/common.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/models/index.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/models/indicators.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/models/slo.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/models/time_window.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/compute_burn_rate.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.test.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/compute_summary_status.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/date_range.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/error_budget.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/get_delay_in_seconds_from_slo.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/get_lookback_date_range.ts delete mode 100644 x-pack/plugins/observability_solution/observability/server/domain/services/validate_slo.ts create mode 100644 x-pack/plugins/observability_solution/slo/common/summary_indices.test.ts create mode 100644 x-pack/plugins/observability_solution/slo/common/summary_indices.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_remote_callout.tsx delete mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_instance_id_query_param.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_get_settings.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_put_slo_settings.tsx create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_remote_badge.tsx rename x-pack/plugins/observability_solution/slo/public/pages/slos/components/{ => slo_list_view}/slo_list_item.stories.tsx (90%) rename x-pack/plugins/observability_solution/slo/public/pages/slos/components/{ => slo_list_view}/slo_list_item.tsx (72%) create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.test.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.ts create mode 100644 x-pack/plugins/observability_solution/slo/server/saved_objects/slo_settings.ts create mode 100644 x-pack/plugins/observability_solution/slo/server/services/__snapshots__/slo_definition_client.test.ts.snap create mode 100644 x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.test.ts create mode 100644 x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.ts create mode 100644 x-pack/plugins/observability_solution/slo/server/services/slo_settings.ts create mode 100644 x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/__snapshots__/remote_summary_doc_to_slo.test.ts.snap create mode 100644 x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.test.ts create mode 100644 x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.ts diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts index dd9f9a3941bb4..a04e762eaf67d 100644 --- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts +++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts @@ -218,6 +218,7 @@ export const HASH_TO_VERSION_MAP = { 'siem-ui-timeline-note|28393dfdeb4e4413393eb5f7ec8c5436': '10.0.0', 'siem-ui-timeline-pinned-event|293fce142548281599060e07ad2c9ddb': '10.0.0', 'siem-ui-timeline|f6739fd4b17646a6c86321a746c247ef': '10.1.0', + 'slo-settings|3d1b76c39bfb2cc8296b024d73854724': '10.0.0', 'slo|dc7f35c0cf07d71bb36f154996fe10c6': '10.1.0', 'space|c3aec2a5d4afcb75554fed96411170e1': '10.0.0', 'spaces-usage-stats|3d1b76c39bfb2cc8296b024d73854724': '10.0.0', diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index bdb2e9e866089..d521bf999bd28 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -912,6 +912,7 @@ "tags", "version" ], + "slo-settings": [], "space": [ "name" ], diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 3682aea98f0c5..f0bf6f2c260cc 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -2996,6 +2996,10 @@ } } }, + "slo-settings": { + "dynamic": false, + "properties": {} + }, "space": { "dynamic": false, "properties": { diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts index 00492d69b8ac5..e9b75ded73d01 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.ts @@ -8,9 +8,9 @@ import * as t from 'io-ts'; import { omitBy, isPlainObject, isEmpty } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; -import { PathReporter } from 'io-ts/lib/PathReporter'; import Boom from '@hapi/boom'; import { strictKeysRt } from '@kbn/io-ts-utils'; +import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { RouteParamsRT } from './typings'; interface KibanaRequestParams { @@ -36,7 +36,7 @@ export function decodeRequestParams( const result = strictKeysRt(paramsRt).decode(paramMap); if (isLeft(result)) { - throw Boom.badRequest(PathReporter.report(result)[0]); + throw Boom.badRequest(formatErrors(result.left).join('|')); } return result.right; diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index a0e9cc288d7b0..f5f84f5114b7d 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -17,7 +17,8 @@ "@kbn/core-http-request-handler-context-server", "@kbn/core-http-server", "@kbn/core-lifecycle-server", - "@kbn/logging" + "@kbn/logging", + "@kbn/securitysolution-io-ts-utils" ], "exclude": [ "target/**/*", diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 7fdf542a8be26..4586b4120d4d3 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -146,6 +146,7 @@ describe('checking migration metadata changes on all registered SO types', () => "siem-ui-timeline-note": "0a32fb776907f596bedca292b8c646496ae9c57b", "siem-ui-timeline-pinned-event": "082daa3ce647b33873f6abccf340bdfa32057c8d", "slo": "9a9995e4572de1839651c43b5fc4dc8276bb5815", + "slo-settings": "f6b5ed339470a6a2cda272bde1750adcf504a11b", "space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e", "spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e", "synthetics-monitor": "5ceb25b6249bd26902c9b34273c71c3dce06dbea", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index ecdc45dd31d22..d8d54319a35df 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -120,6 +120,7 @@ const previouslyRegisteredTypes = [ 'siem-ui-timeline-note', 'siem-ui-timeline-pinned-event', 'slo', + 'slo-settings', 'space', 'spaces-usage-stats', 'synthetics-monitor', diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts index 1a1620f1f7b3a..9993c2ea2f0e5 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts @@ -265,6 +265,7 @@ describe('split .kibana index into multiple system indices', () => { "siem-ui-timeline-note", "siem-ui-timeline-pinned-event", "slo", + "slo-settings", "space", "spaces-usage-stats", "synthetics-monitor", diff --git a/x-pack/packages/kbn-slo-schema/index.ts b/x-pack/packages/kbn-slo-schema/index.ts index 98b183d391bb7..f936a575aa7a7 100644 --- a/x-pack/packages/kbn-slo-schema/index.ts +++ b/x-pack/packages/kbn-slo-schema/index.ts @@ -7,5 +7,4 @@ export * from './src/schema'; export * from './src/rest_specs'; -export * from './src/models/duration'; -export * from './src/models/pagination'; +export * from './src/models'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_error_budget_actions.ts b/x-pack/packages/kbn-slo-schema/src/models/index.ts similarity index 81% rename from x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_error_budget_actions.ts rename to x-pack/packages/kbn-slo-schema/src/models/index.ts index f0d8933615cfb..d281720a1a6a2 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_error_budget_actions.ts +++ b/x-pack/packages/kbn-slo-schema/src/models/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export function useErrorBudgetActions() {} +export * from './pagination'; +export * from './duration'; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/common.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/common.ts new file mode 100644 index 0000000000000..7d5bbcc6025d2 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/common.ts @@ -0,0 +1,21 @@ +/* + * 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 * as t from 'io-ts'; +import { + budgetingMethodSchema, + groupSummarySchema, + objectiveSchema, + timeWindowTypeSchema, +} from '../schema'; + +type BudgetingMethod = t.OutputOf; +type TimeWindowType = t.OutputOf; +type GroupSummary = t.TypeOf; +type Objective = t.OutputOf; + +export type { BudgetingMethod, Objective, TimeWindowType, GroupSummary }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/index.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/index.ts index 78f557bdcbc7d..48a2d88e0c1bb 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/index.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/index.ts @@ -6,3 +6,6 @@ */ export * from './slo'; +export * from './routes'; +export * from './indicators'; +export * from './common'; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/indicators.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/indicators.ts new file mode 100644 index 0000000000000..c7a5aeb7d4d78 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/indicators.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 * as t from 'io-ts'; +import { + apmTransactionDurationIndicatorSchema, + apmTransactionErrorRateIndicatorSchema, + histogramIndicatorSchema, + indicatorSchema, + indicatorTypesSchema, + kqlCustomIndicatorSchema, + kqlWithFiltersSchema, + metricCustomIndicatorSchema, + querySchema, + syntheticsAvailabilityIndicatorSchema, + timesliceMetricBasicMetricWithField, + timesliceMetricDocCountMetric, + timesliceMetricIndicatorSchema, + timesliceMetricPercentileMetric, +} from '../schema'; + +type IndicatorType = t.OutputOf; +type Indicator = t.OutputOf; + +type APMTransactionErrorRateIndicator = t.OutputOf; +type APMTransactionDurationIndicator = t.OutputOf; + +type SyntheticsAvailabilityIndicator = t.OutputOf; + +type MetricCustomIndicator = t.OutputOf; +type TimesliceMetricIndicator = t.OutputOf; +type TimesliceMetricBasicMetricWithField = t.OutputOf; +type TimesliceMetricDocCountMetric = t.OutputOf; +type TimesclieMetricPercentileMetric = t.OutputOf; + +type HistogramIndicator = t.OutputOf; + +type KQLCustomIndicator = t.OutputOf; +type KqlWithFiltersSchema = t.TypeOf; +type QuerySchema = t.TypeOf; + +export type { + APMTransactionDurationIndicator, + APMTransactionErrorRateIndicator, + SyntheticsAvailabilityIndicator, + IndicatorType, + Indicator, + MetricCustomIndicator, + TimesliceMetricIndicator, + TimesliceMetricBasicMetricWithField, + TimesclieMetricPercentileMetric, + TimesliceMetricDocCountMetric, + HistogramIndicator, + KQLCustomIndicator, + KqlWithFiltersSchema, + QuerySchema, +}; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/create.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/create.ts new file mode 100644 index 0000000000000..1abe2a1e330df --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/create.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 * as t from 'io-ts'; +import { indicatorSchema, timeWindowSchema } from '../../schema'; +import { allOrAnyStringOrArray } from '../../schema/common'; +import { + budgetingMethodSchema, + objectiveSchema, + optionalSettingsSchema, + sloIdSchema, + tagsSchema, +} from '../../schema/slo'; + +const createSLOParamsSchema = t.type({ + body: t.intersection([ + t.type({ + name: t.string, + description: t.string, + indicator: indicatorSchema, + timeWindow: timeWindowSchema, + budgetingMethod: budgetingMethodSchema, + objective: objectiveSchema, + }), + t.partial({ + id: sloIdSchema, + settings: optionalSettingsSchema, + tags: tagsSchema, + groupBy: allOrAnyStringOrArray, + revision: t.number, + }), + ]), +}); + +const createSLOResponseSchema = t.type({ + id: sloIdSchema, +}); + +type CreateSLOInput = t.OutputOf; // Raw payload sent by the frontend +type CreateSLOParams = t.TypeOf; // Parsed payload used by the backend +type CreateSLOResponse = t.TypeOf; // Raw response sent to the frontend + +export { createSLOParamsSchema, createSLOResponseSchema }; +export type { CreateSLOInput, CreateSLOParams, CreateSLOResponse }; diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/index.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/delete.ts similarity index 55% rename from x-pack/plugins/observability_solution/observability/server/domain/services/index.ts rename to x-pack/packages/kbn-slo-schema/src/rest_specs/routes/delete.ts index 212647f27b172..80c4094fc58ab 100644 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/index.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/delete.ts @@ -4,10 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import * as t from 'io-ts'; +import { sloIdSchema } from '../../schema/slo'; -export * from './compute_burn_rate'; -export * from './error_budget'; -export * from './compute_sli'; -export * from './compute_summary_status'; -export * from './date_range'; -export * from './validate_slo'; +const deleteSLOParamsSchema = t.type({ + path: t.type({ + id: sloIdSchema, + }), +}); + +export { deleteSLOParamsSchema }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/delete_instance.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/delete_instance.ts new file mode 100644 index 0000000000000..89265a5871325 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/delete_instance.ts @@ -0,0 +1,19 @@ +/* + * 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 * as t from 'io-ts'; +import { sloIdSchema } from '../../schema/slo'; + +const deleteSLOInstancesParamsSchema = t.type({ + body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })) }), +}); + +type DeleteSLOInstancesInput = t.OutputOf; +type DeleteSLOInstancesParams = t.TypeOf; + +export { deleteSLOInstancesParamsSchema }; +export type { DeleteSLOInstancesInput, DeleteSLOInstancesParams }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts new file mode 100644 index 0000000000000..c60fe499fdec5 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts @@ -0,0 +1,68 @@ +/* + * 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 * as t from 'io-ts'; +import { + budgetingMethodSchema, + objectiveSchema, + sloIdSchema, + timeWindowSchema, +} from '../../schema'; +import { + allOrAnyString, + allOrAnyStringOrArray, + dateType, + summarySchema, +} from '../../schema/common'; + +const fetchHistoricalSummaryParamsSchema = t.type({ + body: t.type({ + list: t.array( + t.intersection([ + t.type({ + sloId: sloIdSchema, + instanceId: t.string, + timeWindow: timeWindowSchema, + budgetingMethod: budgetingMethodSchema, + objective: objectiveSchema, + groupBy: allOrAnyStringOrArray, + revision: t.number, + }), + t.partial({ remoteName: t.string }), + ]) + ), + }), +}); + +const historicalSummarySchema = t.intersection([ + t.type({ + date: dateType, + }), + summarySchema, +]); + +const fetchHistoricalSummaryResponseSchema = t.array( + t.type({ + sloId: sloIdSchema, + instanceId: allOrAnyString, + data: t.array(historicalSummarySchema), + }) +); + +type FetchHistoricalSummaryParams = t.TypeOf; +type FetchHistoricalSummaryResponse = t.OutputOf; +type HistoricalSummaryResponse = t.OutputOf; + +export { + fetchHistoricalSummaryParamsSchema, + fetchHistoricalSummaryResponseSchema, + historicalSummarySchema, +}; +export type { + FetchHistoricalSummaryParams, + FetchHistoricalSummaryResponse, + HistoricalSummaryResponse, +}; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find.ts new file mode 100644 index 0000000000000..6d6ebf7b553b0 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find.ts @@ -0,0 +1,40 @@ +/* + * 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 * as t from 'io-ts'; +import { sloWithDataResponseSchema } from '../slo'; + +const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]); +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({ + filters: t.string, + kqlQuery: t.string, + page: t.string, + perPage: t.string, + sortBy: sortBySchema, + sortDirection: sortDirectionSchema, + }), +}); + +const findSLOResponseSchema = t.type({ + page: t.number, + perPage: t.number, + total: t.number, + results: t.array(sloWithDataResponseSchema), +}); + +type FindSLOParams = t.TypeOf; +type FindSLOResponse = t.OutputOf; + +export { findSLOParamsSchema, findSLOResponseSchema }; +export type { FindSLOParams, FindSLOResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_definition.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_definition.ts new file mode 100644 index 0000000000000..0e053a520f70e --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_definition.ts @@ -0,0 +1,31 @@ +/* + * 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 { toBooleanRt } from '@kbn/io-ts-utils/src/to_boolean_rt'; +import * as t from 'io-ts'; +import { sloDefinitionSchema } from '../../schema'; + +const findSloDefinitionsParamsSchema = t.partial({ + query: t.partial({ + search: t.string, + includeOutdatedOnly: toBooleanRt, + page: t.string, + perPage: t.string, + }), +}); + +const findSloDefinitionsResponseSchema = t.type({ + page: t.number, + perPage: t.number, + total: t.number, + results: t.array(sloDefinitionSchema), +}); + +type FindSLODefinitionsParams = t.TypeOf; +type FindSLODefinitionsResponse = t.OutputOf; + +export { findSloDefinitionsParamsSchema, findSloDefinitionsResponseSchema }; +export type { FindSLODefinitionsParams, FindSLODefinitionsResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts new file mode 100644 index 0000000000000..4294dde52b4ce --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; +import { groupSummarySchema } from '../../schema/common'; + +const groupBySchema = t.union([ + t.literal('ungrouped'), + t.literal('slo.tags'), + t.literal('status'), + t.literal('slo.indicator.type'), + t.literal('_index'), +]); + +const findSLOGroupsParamsSchema = t.partial({ + query: t.partial({ + page: t.string, + perPage: t.string, + groupBy: groupBySchema, + groupsFilter: t.union([t.array(t.string), t.string]), + kqlQuery: t.string, + filters: t.string, + }), +}); + +const sloGroupWithSummaryResponseSchema = t.type({ + group: t.string, + groupBy: t.string, + summary: groupSummarySchema, +}); + +const findSLOGroupsResponseSchema = t.type({ + page: t.number, + perPage: t.number, + total: t.number, + results: t.array(sloGroupWithSummaryResponseSchema), +}); + +type FindSLOGroupsParams = t.TypeOf; +type FindSLOGroupsResponse = t.OutputOf; + +export { + findSLOGroupsParamsSchema, + findSLOGroupsResponseSchema, + sloGroupWithSummaryResponseSchema, +}; +export type { FindSLOGroupsParams, FindSLOGroupsResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get.ts new file mode 100644 index 0000000000000..d43b11368a0c1 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get.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 * as t from 'io-ts'; +import { allOrAnyString } from '../../schema/common'; +import { sloIdSchema } from '../../schema/slo'; +import { sloWithDataResponseSchema } from '../slo'; + +const getSLOQuerySchema = t.partial({ + query: t.partial({ + instanceId: allOrAnyString, + remoteName: t.string, + }), +}); + +const getSLOParamsSchema = t.intersection([ + t.type({ + path: t.type({ + id: sloIdSchema, + }), + }), + getSLOQuerySchema, +]); + +const getSLOResponseSchema = sloWithDataResponseSchema; + +type GetSLOParams = t.TypeOf; +type GetSLOResponse = t.OutputOf; + +export { getSLOParamsSchema, getSLOResponseSchema }; +export type { GetSLOParams, GetSLOResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_burn_rates.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_burn_rates.ts new file mode 100644 index 0000000000000..44a7b1e5cba18 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_burn_rates.ts @@ -0,0 +1,40 @@ +/* + * 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 * as t from 'io-ts'; +import { durationType } from '../../schema'; +import { allOrAnyString } from '../../schema/common'; + +const getSLOBurnRatesResponseSchema = t.type({ + burnRates: t.array( + t.type({ + name: t.string, + burnRate: t.number, + sli: t.number, + }) + ), +}); + +const getSLOBurnRatesParamsSchema = t.type({ + path: t.type({ id: t.string }), + body: t.intersection([ + t.type({ + instanceId: allOrAnyString, + windows: t.array( + t.type({ + name: t.string, + duration: durationType, + }) + ), + }), + t.partial({ remoteName: t.string }), + ]), +}); + +type GetSLOBurnRatesResponse = t.OutputOf; + +export { getSLOBurnRatesParamsSchema, getSLOBurnRatesResponseSchema }; +export type { GetSLOBurnRatesResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_instances.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_instances.ts new file mode 100644 index 0000000000000..135c2f7d6d66f --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_instances.ts @@ -0,0 +1,21 @@ +/* + * 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 * as t from 'io-ts'; + +const getSLOInstancesParamsSchema = t.type({ + path: t.type({ id: t.string }), +}); + +const getSLOInstancesResponseSchema = t.type({ + groupBy: t.union([t.string, t.array(t.string)]), + instances: t.array(t.string), +}); + +type GetSLOInstancesResponse = t.OutputOf; + +export { getSLOInstancesParamsSchema, getSLOInstancesResponseSchema }; +export type { GetSLOInstancesResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_preview_data.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_preview_data.ts new file mode 100644 index 0000000000000..53a477fcb67f1 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/get_preview_data.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; +import { indicatorSchema, objectiveSchema } from '../../schema'; +import { dateType } from '../../schema/common'; + +const getPreviewDataParamsSchema = t.type({ + body: t.intersection([ + t.type({ + indicator: indicatorSchema, + range: t.type({ + start: t.number, + end: t.number, + }), + }), + t.partial({ + objective: objectiveSchema, + instanceId: t.string, + groupBy: t.string, + remoteName: t.string, + groupings: t.record(t.string, t.unknown), + }), + ]), +}); + +const getPreviewDataResponseSchema = t.array( + t.intersection([ + t.type({ + date: dateType, + sliValue: t.number, + }), + t.partial({ + events: t.type({ + good: t.number, + bad: t.number, + total: t.number, + }), + }), + ]) +); + +type GetPreviewDataParams = t.TypeOf; +type GetPreviewDataResponse = t.OutputOf; + +export { getPreviewDataParamsSchema, getPreviewDataResponseSchema }; +export type { GetPreviewDataParams, GetPreviewDataResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/index.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/index.ts new file mode 100644 index 0000000000000..afa90877253e3 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/index.ts @@ -0,0 +1,22 @@ +/* + * 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 * from './create'; +export * from './update'; +export * from './delete'; +export * from './find'; +export * from './find_group'; +export * from './find_definition'; +export * from './get'; +export * from './get_burn_rates'; +export * from './get_instances'; +export * from './get_preview_data'; +export * from './reset'; +export * from './manage'; +export * from './delete_instance'; +export * from './fetch_historical_summary'; +export * from './put_settings'; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/manage.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/manage.ts new file mode 100644 index 0000000000000..7a531a2d2fe25 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/manage.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; +import { sloIdSchema } from '../../schema/slo'; + +const manageSLOParamsSchema = t.type({ + path: t.type({ id: sloIdSchema }), +}); + +type ManageSLOParams = t.TypeOf; + +export { manageSLOParamsSchema }; +export type { ManageSLOParams }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/put_settings.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/put_settings.ts new file mode 100644 index 0000000000000..ff5e4a1d98051 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/put_settings.ts @@ -0,0 +1,21 @@ +/* + * 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 * as t from 'io-ts'; +import { sloSettingsSchema } from '../../schema/settings'; + +const putSLOSettingsParamsSchema = t.type({ + body: sloSettingsSchema, +}); + +const putSLOSettingsResponseSchema = sloSettingsSchema; + +type PutSLOSettingsParams = t.TypeOf; +type PutSLOSettingsResponse = t.OutputOf; +type GetSLOSettingsResponse = t.OutputOf; + +export { putSLOSettingsParamsSchema, putSLOSettingsResponseSchema }; +export type { PutSLOSettingsParams, PutSLOSettingsResponse, GetSLOSettingsResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/reset.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/reset.ts new file mode 100644 index 0000000000000..5f12ed6d6d789 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/reset.ts @@ -0,0 +1,20 @@ +/* + * 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 * as t from 'io-ts'; +import { sloDefinitionSchema, sloIdSchema } from '../../schema/slo'; + +const resetSLOParamsSchema = t.type({ + path: t.type({ id: sloIdSchema }), +}); + +const resetSLOResponseSchema = sloDefinitionSchema; + +type ResetSLOParams = t.TypeOf; +type ResetSLOResponse = t.OutputOf; + +export { resetSLOParamsSchema, resetSLOResponseSchema }; +export type { ResetSLOParams, ResetSLOResponse }; diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/update.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/update.ts new file mode 100644 index 0000000000000..d80dfab7d0792 --- /dev/null +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/update.ts @@ -0,0 +1,43 @@ +/* + * 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 * as t from 'io-ts'; +import { indicatorSchema, timeWindowSchema } from '../../schema'; +import { allOrAnyStringOrArray } from '../../schema/common'; +import { + budgetingMethodSchema, + objectiveSchema, + optionalSettingsSchema, + sloDefinitionSchema, + sloIdSchema, + tagsSchema, +} from '../../schema/slo'; + +const updateSLOParamsSchema = t.type({ + path: t.type({ + id: sloIdSchema, + }), + body: t.partial({ + name: t.string, + description: t.string, + indicator: indicatorSchema, + timeWindow: timeWindowSchema, + budgetingMethod: budgetingMethodSchema, + objective: objectiveSchema, + settings: optionalSettingsSchema, + tags: tagsSchema, + groupBy: allOrAnyStringOrArray, + }), +}); + +const updateSLOResponseSchema = sloDefinitionSchema; + +type UpdateSLOInput = t.OutputOf; +type UpdateSLOParams = t.TypeOf; +type UpdateSLOResponse = t.OutputOf; + +export { updateSLOParamsSchema, updateSLOResponseSchema }; +export type { UpdateSLOInput, UpdateSLOParams, UpdateSLOResponse }; 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 92f52accf5e6b..6fc093e054055 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts @@ -6,428 +6,27 @@ */ import * as t from 'io-ts'; -import { toBooleanRt } from '@kbn/io-ts-utils'; import { allOrAnyString, - apmTransactionDurationIndicatorSchema, - apmTransactionErrorRateIndicatorSchema, - syntheticsAvailabilityIndicatorSchema, - budgetingMethodSchema, - dateType, - durationType, groupingsSchema, - histogramIndicatorSchema, - historicalSummarySchema, - indicatorSchema, - indicatorTypesSchema, - kqlCustomIndicatorSchema, - metricCustomIndicatorSchema, metaSchema, - timesliceMetricIndicatorSchema, - objectiveSchema, - optionalSettingsSchema, - previewDataSchema, - settingsSchema, - sloIdSchema, + remoteSchema, + sloDefinitionSchema, summarySchema, - groupSummarySchema, - tagsSchema, - timeWindowSchema, - timeWindowTypeSchema, - timesliceMetricBasicMetricWithField, - timesliceMetricDocCountMetric, - timesliceMetricPercentileMetric, - allOrAnyStringOrArray, - kqlWithFiltersSchema, - querySchema, } from '../schema'; -const createSLOParamsSchema = t.type({ - body: t.intersection([ - t.type({ - name: t.string, - description: t.string, - indicator: indicatorSchema, - timeWindow: timeWindowSchema, - budgetingMethod: budgetingMethodSchema, - objective: objectiveSchema, - }), - t.partial({ - id: sloIdSchema, - settings: optionalSettingsSchema, - tags: tagsSchema, - groupBy: allOrAnyStringOrArray, - revision: t.number, - }), - ]), -}); - -const createSLOResponseSchema = t.type({ - id: sloIdSchema, -}); - -const getPreviewDataParamsSchema = t.type({ - body: t.intersection([ - t.type({ - indicator: indicatorSchema, - range: t.type({ - start: t.number, - end: t.number, - }), - }), - t.partial({ - objective: objectiveSchema, - instanceId: t.string, - groupBy: t.string, - groupings: t.record(t.string, t.unknown), - }), - ]), -}); - -const getPreviewDataResponseSchema = t.array(previewDataSchema); - -const deleteSLOParamsSchema = t.type({ - path: t.type({ - id: sloIdSchema, - }), -}); - -const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]); -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({ - filters: t.string, - kqlQuery: t.string, - page: t.string, - perPage: t.string, - sortBy: sortBySchema, - sortDirection: sortDirectionSchema, - }), -}); - -const groupBySchema = t.union([ - t.literal('ungrouped'), - t.literal('slo.tags'), - t.literal('status'), - t.literal('slo.indicator.type'), -]); - -const findSLOGroupsParamsSchema = t.partial({ - query: t.partial({ - page: t.string, - perPage: t.string, - groupBy: groupBySchema, - groupsFilter: t.union([t.array(t.string), t.string]), - kqlQuery: t.string, - filters: t.string, - }), -}); - -const sloResponseSchema = t.intersection([ - t.type({ - id: sloIdSchema, - name: t.string, - description: t.string, - indicator: indicatorSchema, - timeWindow: timeWindowSchema, - budgetingMethod: budgetingMethodSchema, - objective: objectiveSchema, - revision: t.number, - settings: settingsSchema, - enabled: t.boolean, - tags: tagsSchema, - groupBy: allOrAnyStringOrArray, - createdAt: dateType, - updatedAt: dateType, - version: t.number, - }), +const sloWithDataResponseSchema = t.intersection([ + sloDefinitionSchema, + t.type({ summary: summarySchema, groupings: groupingsSchema }), t.partial({ instanceId: allOrAnyString, + meta: metaSchema, + remote: remoteSchema, }), ]); -const sloWithSummaryResponseSchema = t.intersection([ - sloResponseSchema, - t.intersection([ - t.type({ summary: summarySchema, groupings: groupingsSchema }), - t.partial({ meta: metaSchema }), - ]), -]); - -const sloGroupWithSummaryResponseSchema = t.type({ - group: t.string, - groupBy: t.string, - summary: groupSummarySchema, -}); - -const getSLOQuerySchema = t.partial({ - query: t.partial({ - instanceId: allOrAnyString, - }), -}); -const getSLOParamsSchema = t.intersection([ - t.type({ - path: t.type({ - id: sloIdSchema, - }), - }), - getSLOQuerySchema, -]); - -const getSLOResponseSchema = sloWithSummaryResponseSchema; - -const updateSLOParamsSchema = t.type({ - path: t.type({ - id: sloIdSchema, - }), - body: t.partial({ - name: t.string, - description: t.string, - indicator: indicatorSchema, - timeWindow: timeWindowSchema, - budgetingMethod: budgetingMethodSchema, - objective: objectiveSchema, - settings: optionalSettingsSchema, - tags: tagsSchema, - groupBy: allOrAnyStringOrArray, - }), -}); - -const manageSLOParamsSchema = t.type({ - path: t.type({ id: sloIdSchema }), -}); - -const resetSLOParamsSchema = t.type({ - path: t.type({ id: sloIdSchema }), -}); - -const resetSLOResponseSchema = sloResponseSchema; - -const updateSLOResponseSchema = sloResponseSchema; - -const findSLOResponseSchema = t.type({ - page: t.number, - perPage: t.number, - total: t.number, - results: t.array(sloWithSummaryResponseSchema), -}); - -const findSLOGroupsResponseSchema = t.type({ - page: t.number, - perPage: t.number, - total: t.number, - results: t.array(sloGroupWithSummaryResponseSchema), -}); - -const deleteSLOInstancesParamsSchema = t.type({ - body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })) }), -}); - -const fetchHistoricalSummaryParamsSchema = t.type({ - body: t.type({ - list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })), - }), -}); - -const fetchHistoricalSummaryResponseSchema = t.array( - t.type({ - sloId: sloIdSchema, - instanceId: allOrAnyString, - data: t.array(historicalSummarySchema), - }) -); - -const findSloDefinitionsParamsSchema = t.partial({ - query: t.partial({ - search: t.string, - includeOutdatedOnly: toBooleanRt, - page: t.string, - perPage: t.string, - }), -}); - -const findSloDefinitionsResponseSchema = t.type({ - page: t.number, - perPage: t.number, - total: t.number, - results: t.array(sloResponseSchema), -}); - -const getSLOBurnRatesResponseSchema = t.type({ - burnRates: t.array( - t.type({ - name: t.string, - burnRate: t.number, - sli: t.number, - }) - ), -}); - -const getSLOBurnRatesParamsSchema = t.type({ - path: t.type({ id: t.string }), - body: t.type({ - instanceId: allOrAnyString, - windows: t.array( - t.type({ - name: t.string, - duration: durationType, - }) - ), - }), -}); - -const getSLOInstancesParamsSchema = t.type({ - path: t.type({ id: t.string }), -}); - -const getSLOInstancesResponseSchema = t.type({ - groupBy: t.union([t.string, t.array(t.string)]), - instances: t.array(t.string), -}); - -type SLOResponse = t.OutputOf; -type SLOWithSummaryResponse = t.OutputOf; - -type SLOGroupWithSummaryResponse = t.OutputOf; - -type CreateSLOInput = t.OutputOf; // Raw payload sent by the frontend -type CreateSLOParams = t.TypeOf; // Parsed payload used by the backend -type CreateSLOResponse = t.TypeOf; // Raw response sent to the frontend - -type GetSLOParams = t.TypeOf; -type GetSLOResponse = t.OutputOf; - -type ManageSLOParams = t.TypeOf; - -type ResetSLOParams = t.TypeOf; -type ResetSLOResponse = t.OutputOf; - -type UpdateSLOInput = t.OutputOf; -type UpdateSLOParams = t.TypeOf; -type UpdateSLOResponse = t.OutputOf; - -type FindSLOParams = t.TypeOf; -type FindSLOResponse = t.OutputOf; - -type FindSLOGroupsParams = t.TypeOf; -type FindSLOGroupsResponse = t.OutputOf; - -type DeleteSLOInstancesInput = t.OutputOf; -type DeleteSLOInstancesParams = t.TypeOf; - -type FetchHistoricalSummaryParams = t.TypeOf; -type FetchHistoricalSummaryResponse = t.OutputOf; -type HistoricalSummaryResponse = t.OutputOf; - -type FindSLODefinitionsParams = t.TypeOf; -type FindSLODefinitionsResponse = t.OutputOf; - -type GetPreviewDataParams = t.TypeOf; -type GetPreviewDataResponse = t.OutputOf; - -type GetSLOInstancesResponse = t.OutputOf; - -type GetSLOBurnRatesResponse = t.OutputOf; -type BudgetingMethod = t.OutputOf; -type TimeWindow = t.OutputOf; -type IndicatorType = t.OutputOf; -type Indicator = t.OutputOf; -type Objective = t.OutputOf; -type APMTransactionErrorRateIndicator = t.OutputOf; -type APMTransactionDurationIndicator = t.OutputOf; -type SyntheticsAvailabilityIndicator = t.OutputOf; -type MetricCustomIndicator = t.OutputOf; -type TimesliceMetricIndicator = t.OutputOf; -type TimesliceMetricBasicMetricWithField = t.OutputOf; -type TimesliceMetricDocCountMetric = t.OutputOf; -type TimesclieMetricPercentileMetric = t.OutputOf; -type HistogramIndicator = t.OutputOf; -type KQLCustomIndicator = t.OutputOf; -type GroupSummary = t.TypeOf; -type KqlWithFiltersSchema = t.TypeOf; -type QuerySchema = t.TypeOf; +type SLODefinitionResponse = t.OutputOf; +type SLOWithSummaryResponse = t.OutputOf; -export { - createSLOParamsSchema, - deleteSLOParamsSchema, - deleteSLOInstancesParamsSchema, - findSLOParamsSchema, - findSLOResponseSchema, - findSLOGroupsParamsSchema, - findSLOGroupsResponseSchema, - getPreviewDataParamsSchema, - getPreviewDataResponseSchema, - getSLOParamsSchema, - getSLOResponseSchema, - fetchHistoricalSummaryParamsSchema, - fetchHistoricalSummaryResponseSchema, - findSloDefinitionsParamsSchema, - findSloDefinitionsResponseSchema, - manageSLOParamsSchema, - resetSLOParamsSchema, - resetSLOResponseSchema, - sloResponseSchema, - sloWithSummaryResponseSchema, - sloGroupWithSummaryResponseSchema, - updateSLOParamsSchema, - updateSLOResponseSchema, - getSLOBurnRatesParamsSchema, - getSLOBurnRatesResponseSchema, - getSLOInstancesParamsSchema, - getSLOInstancesResponseSchema, -}; -export type { - BudgetingMethod, - CreateSLOInput, - CreateSLOParams, - CreateSLOResponse, - DeleteSLOInstancesInput, - DeleteSLOInstancesParams, - FindSLOParams, - FindSLOResponse, - FindSLOGroupsParams, - FindSLOGroupsResponse, - GetPreviewDataParams, - GetPreviewDataResponse, - GetSLOParams, - GetSLOResponse, - FetchHistoricalSummaryParams, - FetchHistoricalSummaryResponse, - HistoricalSummaryResponse, - FindSLODefinitionsParams, - FindSLODefinitionsResponse, - ManageSLOParams, - ResetSLOParams, - ResetSLOResponse, - SLOResponse, - SLOWithSummaryResponse, - SLOGroupWithSummaryResponse, - UpdateSLOInput, - UpdateSLOParams, - UpdateSLOResponse, - APMTransactionDurationIndicator, - APMTransactionErrorRateIndicator, - SyntheticsAvailabilityIndicator, - GetSLOBurnRatesResponse, - GetSLOInstancesResponse, - IndicatorType, - Indicator, - Objective, - MetricCustomIndicator, - TimesliceMetricIndicator, - TimesliceMetricBasicMetricWithField, - TimesclieMetricPercentileMetric, - TimesliceMetricDocCountMetric, - HistogramIndicator, - KQLCustomIndicator, - TimeWindow, - GroupSummary, - KqlWithFiltersSchema, - QuerySchema, -}; +export { sloWithDataResponseSchema }; +export type { SLODefinitionResponse, SLOWithSummaryResponse }; 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 350caed40d2d4..155164dee593f 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/common.ts @@ -59,6 +59,11 @@ const metaSchema = t.partial({ }), }); +const remoteSchema = t.type({ + remoteName: t.string, + kibanaUrl: t.string, +}); + const groupSummarySchema = t.type({ total: t.number, worst: t.type({ @@ -76,58 +81,8 @@ const groupSummarySchema = t.type({ noData: t.number, }); -const historicalSummarySchema = t.intersection([ - t.type({ - date: dateType, - }), - summarySchema, -]); - -const previewDataSchema = t.intersection([ - t.type({ - date: dateType, - sliValue: t.number, - }), - t.partial({ - events: t.type({ - good: t.number, - bad: t.number, - total: t.number, - }), - }), -]); - const dateRangeSchema = t.type({ from: dateType, to: dateType }); -const kqlQuerySchema = t.string; - -const kqlWithFiltersSchema = t.type({ - kqlQuery: t.string, - filters: t.array( - t.type({ - meta: t.partial({ - alias: t.union([t.string, t.null]), - disabled: t.boolean, - negate: t.boolean, - // controlledBy is there to identify who owns the filter - controlledBy: t.string, - // allows grouping of filters - group: t.string, - // index and type are optional only because when you create a new filter, there are no defaults - index: t.string, - isMultiIndex: t.boolean, - type: t.string, - key: t.string, - params: t.any, - value: t.string, - }), - query: t.record(t.string, t.any), - }) - ), -}); - -const querySchema = t.union([kqlQuerySchema, kqlWithFiltersSchema]); - export { ALL_VALUE, allOrAnyString, @@ -136,13 +91,9 @@ export { dateType, errorBudgetSchema, groupingsSchema, - historicalSummarySchema, - previewDataSchema, statusSchema, summarySchema, metaSchema, groupSummarySchema, - kqlWithFiltersSchema, - querySchema, - kqlQuerySchema, + remoteSchema, }; diff --git a/x-pack/packages/kbn-slo-schema/src/schema/index.ts b/x-pack/packages/kbn-slo-schema/src/schema/index.ts index 2fbddc7ce8537..5fdaa72f71527 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/index.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/index.ts @@ -10,3 +10,4 @@ export * from './duration'; export * from './indicators'; export * from './time_window'; export * from './slo'; +export * from './settings'; diff --git a/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts b/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts index 189857be733f7..7123ee7a9575b 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts @@ -6,7 +6,36 @@ */ import * as t from 'io-ts'; -import { allOrAnyString, dateRangeSchema, querySchema } from './common'; +import { allOrAnyString, dateRangeSchema } from './common'; + +const kqlQuerySchema = t.string; + +const kqlWithFiltersSchema = t.type({ + kqlQuery: t.string, + filters: t.array( + t.type({ + meta: t.partial({ + alias: t.union([t.string, t.null]), + disabled: t.boolean, + negate: t.boolean, + // controlledBy is there to identify who owns the filter + controlledBy: t.string, + // allows grouping of filters + group: t.string, + // index and type are optional only because when you create a new filter, there are no defaults + index: t.string, + isMultiIndex: t.boolean, + type: t.string, + key: t.string, + params: t.any, + value: t.string, + }), + query: t.record(t.string, t.any), + }) + ), +}); + +const querySchema = t.union([kqlQuerySchema, kqlWithFiltersSchema]); const apmTransactionDurationIndicatorTypeSchema = t.literal('sli.apm.transactionDuration'); const apmTransactionDurationIndicatorSchema = t.type({ @@ -288,6 +317,9 @@ const indicatorSchema = t.union([ ]); export { + kqlQuerySchema, + kqlWithFiltersSchema, + querySchema, apmTransactionDurationIndicatorSchema, apmTransactionDurationIndicatorTypeSchema, apmTransactionErrorRateIndicatorSchema, diff --git a/x-pack/plugins/observability_solution/observability/server/domain/models/error_budget.ts b/x-pack/packages/kbn-slo-schema/src/schema/settings.ts similarity index 67% rename from x-pack/plugins/observability_solution/observability/server/domain/models/error_budget.ts rename to x-pack/packages/kbn-slo-schema/src/schema/settings.ts index 50ca6e469d7dd..5eb7e7b23abf3 100644 --- a/x-pack/plugins/observability_solution/observability/server/domain/models/error_budget.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/settings.ts @@ -6,8 +6,8 @@ */ import * as t from 'io-ts'; -import { errorBudgetSchema } from '@kbn/slo-schema'; -type ErrorBudget = t.TypeOf; - -export type { ErrorBudget }; +export const sloSettingsSchema = t.type({ + useAllRemoteClusters: t.boolean, + selectedRemoteClusters: t.array(t.string), +}); diff --git a/x-pack/packages/kbn-slo-schema/src/schema/slo.ts b/x-pack/packages/kbn-slo-schema/src/schema/slo.ts index 6f39215a5b5ab..692ec16050353 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/slo.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/slo.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { allOrAnyStringOrArray, dateType, summarySchema, groupingsSchema } from './common'; +import { allOrAnyStringOrArray, dateType } from './common'; import { durationType } from './duration'; import { indicatorSchema } from './indicators'; import { timeWindowSchema } from './time_window'; @@ -31,11 +31,13 @@ const settingsSchema = t.type({ frequency: durationType, }); +const groupBySchema = allOrAnyStringOrArray; + const optionalSettingsSchema = t.partial({ ...settingsSchema.props }); const tagsSchema = t.array(t.string); const sloIdSchema = t.string; -const sloSchema = t.type({ +const sloDefinitionSchema = t.type({ id: sloIdSchema, name: t.string, description: t.string, @@ -49,24 +51,19 @@ const sloSchema = t.type({ tags: tagsSchema, createdAt: dateType, updatedAt: dateType, - groupBy: allOrAnyStringOrArray, + groupBy: groupBySchema, version: t.number, }); -const sloWithSummarySchema = t.intersection([ - sloSchema, - t.type({ summary: summarySchema, groupings: groupingsSchema }), -]); - export { budgetingMethodSchema, objectiveSchema, + groupBySchema, occurrencesBudgetingMethodSchema, optionalSettingsSchema, settingsSchema, + sloDefinitionSchema, sloIdSchema, - sloSchema, - sloWithSummarySchema, tagsSchema, targetSchema, timeslicesBudgetingMethodSchema, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts index 339f7044583b4..e3d9498e31bdd 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts @@ -270,8 +270,7 @@ describe('createApi', () => { expect(response.custom).toHaveBeenCalledWith({ body: { attributes: { _inspect: [], data: null }, - message: - 'Invalid value 1 supplied to : Partial<{| query: Partial<{| _inspect: pipe(JSON, boolean) |}> |}>/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', + message: 'Invalid value "1" supplied to "query,_inspect"', }, statusCode: 400, }); diff --git a/x-pack/plugins/observability_solution/observability/common/locators/paths.ts b/x-pack/plugins/observability_solution/observability/common/locators/paths.ts index 1646224a63be7..344da4b59bc00 100644 --- a/x-pack/plugins/observability_solution/observability/common/locators/paths.ts +++ b/x-pack/plugins/observability_solution/observability/common/locators/paths.ts @@ -16,6 +16,7 @@ export const RULES_PATH = '/alerts/rules' as const; export const RULES_LOGS_PATH = '/alerts/rules/logs' as const; export const RULE_DETAIL_PATH = '/alerts/rules/:ruleId' as const; export const CASES_PATH = '/cases' as const; +export const SETTINGS_PATH = '/slos/settings' as const; // // SLOs have been moved to its own app (slo). Keeping around for redirecting purposes. export const OLD_SLOS_PATH = '/slos' as const; diff --git a/x-pack/plugins/observability_solution/observability/server/domain/models/common.ts b/x-pack/plugins/observability_solution/observability/server/domain/models/common.ts deleted file mode 100644 index 9e920a7ed9074..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/models/common.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import * as t from 'io-ts'; - -import { - dateRangeSchema, - historicalSummarySchema, - statusSchema, - summarySchema, - groupingsSchema, - groupSummarySchema, - metaSchema, -} from '@kbn/slo-schema'; - -type Status = t.TypeOf; -type DateRange = t.TypeOf; -type HistoricalSummary = t.TypeOf; -type Summary = t.TypeOf; -type Groupings = t.TypeOf; -type Meta = t.TypeOf; -type GroupSummary = t.TypeOf; - -export type { DateRange, Groupings, GroupSummary, HistoricalSummary, Meta, Status, Summary }; diff --git a/x-pack/plugins/observability_solution/observability/server/domain/models/index.ts b/x-pack/plugins/observability_solution/observability/server/domain/models/index.ts deleted file mode 100644 index 16336c40928f8..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/models/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './common'; -export { Duration, DurationUnit, toDurationUnit, toMomentUnitOfTime } from '@kbn/slo-schema'; -export * from './error_budget'; -export * from './indicators'; -export * from './slo'; -export * from './time_window'; diff --git a/x-pack/plugins/observability_solution/observability/server/domain/models/indicators.ts b/x-pack/plugins/observability_solution/observability/server/domain/models/indicators.ts deleted file mode 100644 index 257994bce2f25..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/models/indicators.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { - apmTransactionDurationIndicatorSchema, - apmTransactionErrorRateIndicatorSchema, - syntheticsAvailabilityIndicatorSchema, - indicatorDataSchema, - indicatorSchema, - indicatorTypesSchema, - kqlCustomIndicatorSchema, - metricCustomIndicatorSchema, -} from '@kbn/slo-schema'; - -type APMTransactionErrorRateIndicator = t.TypeOf; -type APMTransactionDurationIndicator = t.TypeOf; -type SyntheticsAvailabilityIndicator = t.TypeOf; -type KQLCustomIndicator = t.TypeOf; -type MetricCustomIndicator = t.TypeOf; -type Indicator = t.TypeOf; -type IndicatorTypes = t.TypeOf; -type IndicatorData = t.TypeOf; - -export type { - Indicator, - IndicatorTypes, - APMTransactionErrorRateIndicator, - APMTransactionDurationIndicator, - SyntheticsAvailabilityIndicator, - KQLCustomIndicator, - MetricCustomIndicator, - IndicatorData, -}; diff --git a/x-pack/plugins/observability_solution/observability/server/domain/models/slo.ts b/x-pack/plugins/observability_solution/observability/server/domain/models/slo.ts deleted file mode 100644 index d17498c3dbfed..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/models/slo.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { sloIdSchema, sloSchema, sloWithSummarySchema } from '@kbn/slo-schema'; - -type SLO = t.TypeOf; -type SLOId = t.TypeOf; -type SLOWithSummary = t.TypeOf; -type StoredSLO = t.OutputOf; - -export type { SLO, SLOWithSummary, SLOId, StoredSLO }; diff --git a/x-pack/plugins/observability_solution/observability/server/domain/models/time_window.ts b/x-pack/plugins/observability_solution/observability/server/domain/models/time_window.ts deleted file mode 100644 index aa12aa70a8ae8..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/models/time_window.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - calendarAlignedTimeWindowSchema, - rollingTimeWindowSchema, - timeWindowSchema, -} from '@kbn/slo-schema'; -import moment from 'moment'; -import * as t from 'io-ts'; - -type TimeWindow = t.TypeOf; -type RollingTimeWindow = t.TypeOf; -type CalendarAlignedTimeWindow = t.TypeOf; - -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_solution/observability/server/domain/services/compute_burn_rate.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/compute_burn_rate.ts deleted file mode 100644 index 2cd758c4c3a74..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/compute_burn_rate.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { toHighPrecision } from '../../utils/number'; -import { IndicatorData, SLO } from '../models'; - -/** - * A Burn Rate is computed with the Indicator Data retrieved from a specific lookback period - * It tells how fast we are consumming our error budget during a specific period - */ -export function computeBurnRate(slo: SLO, sliData: IndicatorData): number { - const { good, total } = sliData; - if (total === 0 || good >= total) { - return 0; - } - - const errorBudget = 1 - slo.objective.target; - const errorRate = 1 - good / total; - return toHighPrecision(errorRate / errorBudget); -} diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.test.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.test.ts deleted file mode 100644 index 79494de1d7cf1..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { computeSLI } from './compute_sli'; - -describe('computeSLI', () => { - it('returns -1 when no total events', () => { - expect(computeSLI(100, 0)).toEqual(-1); - }); - - it('returns the sli value', () => { - expect(computeSLI(100, 1000)).toEqual(0.1); - }); - - it('returns when good is greater than total events', () => { - expect(computeSLI(9999, 9)).toEqual(1111); - }); - - it('returns rounds the value to 6 digits', () => { - expect(computeSLI(33, 90)).toEqual(0.366667); - }); -}); diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.ts deleted file mode 100644 index bafab79104134..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/compute_sli.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { toHighPrecision } from '../../utils/number'; - -const NO_DATA = -1; - -export function computeSLI(good: number, total: number): number { - if (total === 0) { - return NO_DATA; - } - - return toHighPrecision(good / total); -} diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/compute_summary_status.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/compute_summary_status.ts deleted file mode 100644 index 3aaaffe180b6a..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/compute_summary_status.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ErrorBudget, SLO, Status } from '../models'; - -export function computeSummaryStatus(slo: SLO, sliValue: number, errorBudget: ErrorBudget): Status { - if (sliValue === -1) { - return 'NO_DATA'; - } - - if (sliValue >= slo.objective.target) { - return 'HEALTHY'; - } else { - return errorBudget.remaining > 0 ? 'DEGRADING' : 'VIOLATED'; - } -} diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/date_range.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/date_range.ts deleted file mode 100644 index 9c54197aa39e3..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/date_range.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema'; -import { assertNever } from '@kbn/std'; -import moment from 'moment'; -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 = toCalendarAlignedTimeWindowMomentUnit(timeWindow); - const from = moment.utc(currentDate).startOf(unit); - const to = moment.utc(currentDate).endOf(unit); - - return { from: from.toDate(), to: to.toDate() }; - } - - if (rollingTimeWindowSchema.is(timeWindow)) { - 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: from.toDate(), - to: to.toDate(), - }; - } - - assertNever(timeWindow); -}; diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/error_budget.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/error_budget.ts deleted file mode 100644 index 74165c5eec560..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/error_budget.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { toHighPrecision } from '../../utils/number'; -import { ErrorBudget } from '../models'; - -export function toErrorBudget( - initial: number, - consumed: number, - isEstimated: boolean = false -): ErrorBudget { - return { - initial: toHighPrecision(initial), - consumed: toHighPrecision(consumed), - remaining: toHighPrecision(1 - consumed), - isEstimated, - }; -} diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/get_delay_in_seconds_from_slo.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/get_delay_in_seconds_from_slo.ts deleted file mode 100644 index e6cef17750394..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/get_delay_in_seconds_from_slo.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; -import { SLO } from '../models'; - -export function getDelayInSecondsFromSLO(slo: SLO) { - const fixedInterval = timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) - ? slo.objective.timesliceWindow!.asSeconds() - : 60; - const syncDelay = slo.settings.syncDelay.asSeconds(); - const frequency = slo.settings.frequency.asSeconds(); - return fixedInterval + syncDelay + frequency; -} diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/get_lookback_date_range.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/get_lookback_date_range.ts deleted file mode 100644 index 63ed12a7244c0..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/get_lookback_date_range.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { Duration, toMomentUnitOfTime } from '../models'; -export function getLookbackDateRange( - startedAt: Date, - duration: Duration, - delayInSeconds = 0 -): { from: Date; to: Date } { - const unit = toMomentUnitOfTime(duration.unit); - const now = moment(startedAt).subtract(delayInSeconds, 'seconds').startOf('minute'); - const from = now.clone().subtract(duration.value, unit).startOf('minute'); - - return { - from: from.toDate(), - to: now.toDate(), - }; -} diff --git a/x-pack/plugins/observability_solution/observability/server/domain/services/validate_slo.ts b/x-pack/plugins/observability_solution/observability/server/domain/services/validate_slo.ts deleted file mode 100644 index eb253f44cdf5a..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/domain/services/validate_slo.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - timeslicesBudgetingMethodSchema, - Duration, - DurationUnit, - rollingTimeWindowSchema, - calendarAlignedTimeWindowSchema, -} from '@kbn/slo-schema'; -import { IllegalArgumentError } from '../../errors'; -import { SLO } from '../models'; - -/** - * Asserts the SLO is valid from a business invariants point of view. - * e.g. a 'target' objective requires a number between ]0, 1] - * e.g. a 'timeslices' budgeting method requires an objective's timeslice_target to be defined. - * - * @param slo {SLO} - */ -export function validateSLO(slo: SLO) { - if (!isValidId(slo.id)) { - throw new IllegalArgumentError('Invalid id'); - } - - if (!isValidTargetNumber(slo.objective.target)) { - throw new IllegalArgumentError('Invalid objective.target'); - } - - if ( - rollingTimeWindowSchema.is(slo.timeWindow) && - !isValidRollingTimeWindowDuration(slo.timeWindow.duration) - ) { - throw new IllegalArgumentError('Invalid time_window.duration'); - } - - if ( - calendarAlignedTimeWindowSchema.is(slo.timeWindow) && - !isValidCalendarAlignedTimeWindowDuration(slo.timeWindow.duration) - ) { - throw new IllegalArgumentError('Invalid time_window.duration'); - } - - if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) { - if ( - slo.objective.timesliceTarget === undefined || - !isValidTargetNumber(slo.objective.timesliceTarget) - ) { - throw new IllegalArgumentError('Invalid objective.timeslice_target'); - } - - if ( - slo.objective.timesliceWindow === undefined || - !isValidTimesliceWindowDuration(slo.objective.timesliceWindow, slo.timeWindow.duration) - ) { - throw new IllegalArgumentError('Invalid objective.timeslice_window'); - } - } - - validateSettings(slo); -} - -function validateSettings(slo: SLO) { - if (!isValidFrequencySettings(slo.settings.frequency)) { - throw new IllegalArgumentError('Invalid settings.frequency'); - } - - if (!isValidSyncDelaySettings(slo.settings.syncDelay)) { - throw new IllegalArgumentError('Invalid settings.sync_delay'); - } -} - -function isValidId(id: string): boolean { - const MIN_ID_LENGTH = 8; - const MAX_ID_LENGTH = 36; - return MIN_ID_LENGTH <= id.length && id.length <= MAX_ID_LENGTH; -} - -function isValidTargetNumber(value: number): boolean { - return value > 0 && value < 1; -} - -function isValidRollingTimeWindowDuration(duration: Duration): boolean { - // 7, 30 or 90days accepted - return duration.unit === DurationUnit.Day && [7, 30, 90].includes(duration.value); -} - -function isValidCalendarAlignedTimeWindowDuration(duration: Duration): boolean { - // 1 week or 1 month - return [DurationUnit.Week, DurationUnit.Month].includes(duration.unit) && duration.value === 1; -} - -function isValidTimesliceWindowDuration(timesliceWindow: Duration, timeWindow: Duration): boolean { - return ( - [DurationUnit.Minute, DurationUnit.Hour].includes(timesliceWindow.unit) && - timesliceWindow.isShorterThan(timeWindow) - ); -} - -/** - * validate that 1 minute <= frequency < 1 hour - */ -function isValidFrequencySettings(frequency: Duration): boolean { - return ( - frequency.isLongerOrEqualThan(new Duration(1, DurationUnit.Minute)) && - frequency.isShorterThan(new Duration(1, DurationUnit.Hour)) - ); -} - -/** - * validate that 1 minute <= sync_delay < 6 hour - */ -function isValidSyncDelaySettings(syncDelay: Duration): boolean { - return ( - syncDelay.isLongerOrEqualThan(new Duration(1, DurationUnit.Minute)) && - syncDelay.isShorterThan(new Duration(6, DurationUnit.Hour)) - ); -} diff --git a/x-pack/plugins/observability_solution/observability/server/utils/queries.ts b/x-pack/plugins/observability_solution/observability/server/utils/queries.ts index bdacad577838c..fa581df62e745 100644 --- a/x-pack/plugins/observability_solution/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability_solution/observability/server/utils/queries.ts @@ -101,3 +101,7 @@ export async function typedSearch< ): Promise> { return (await esClient.search(params)) as unknown as ESSearchResponse; } + +export function createEsParams(params: T): T { + return params; +} diff --git a/x-pack/plugins/observability_solution/slo/common/locators/paths.ts b/x-pack/plugins/observability_solution/slo/common/locators/paths.ts index 7b90f861f315c..2f2a1b329543b 100644 --- a/x-pack/plugins/observability_solution/slo/common/locators/paths.ts +++ b/x-pack/plugins/observability_solution/slo/common/locators/paths.ts @@ -12,21 +12,23 @@ export const SLO_DETAIL_PATH = '/:sloId' as const; export const SLO_CREATE_PATH = '/create' as const; export const SLO_EDIT_PATH = '/edit/:sloId' as const; export const SLOS_OUTDATED_DEFINITIONS_PATH = '/outdated-definitions' as const; +export const SLO_SETTINGS_PATH = '/settings' as const; export const paths = { slos: `${SLOS_BASE_PATH}${SLOS_PATH}`, + slosSettings: `${SLOS_BASE_PATH}${SLO_SETTINGS_PATH}`, slosWelcome: `${SLOS_BASE_PATH}${SLOS_WELCOME_PATH}`, slosOutdatedDefinitions: `${SLOS_BASE_PATH}${SLOS_OUTDATED_DEFINITIONS_PATH}`, sloCreate: `${SLOS_BASE_PATH}${SLO_CREATE_PATH}`, sloCreateWithEncodedForm: (encodedParams: string) => `${SLOS_BASE_PATH}${SLO_CREATE_PATH}?_a=${encodedParams}`, - sloEdit: (sloId: string) => `${SLOS_BASE_PATH}${SLOS_PATH}/edit/${encodeURIComponent(sloId)}`, + sloEdit: (sloId: string) => `${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}`, sloEditWithEncodedForm: (sloId: string, encodedParams: string) => - `${SLOS_BASE_PATH}${SLOS_PATH}/edit/${encodeURIComponent(sloId)}?_a=${encodedParams}`, - sloDetails: (sloId: string, instanceId?: string) => - !!instanceId - ? `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}?instanceId=${encodeURIComponent( - instanceId - )}` - : `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}`, + `${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}?_a=${encodedParams}`, + sloDetails: (sloId: string, instanceId?: string, remoteName?: string) => { + const qs = new URLSearchParams(); + if (!!instanceId) qs.append('instanceId', instanceId); + if (!!remoteName) qs.append('remoteName', remoteName); + return `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}?${qs.toString()}`; + }, }; diff --git a/x-pack/plugins/observability_solution/slo/common/summary_indices.test.ts b/x-pack/plugins/observability_solution/slo/common/summary_indices.test.ts new file mode 100644 index 0000000000000..d55107dfcd802 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/common/summary_indices.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getListOfSloSummaryIndices } from './summary_indices'; +import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from './constants'; + +describe('getListOfSloSummaryIndices', () => { + it('should return default index if disabled', function () { + const settings = { + useAllRemoteClusters: false, + selectedRemoteClusters: [], + }; + const result = getListOfSloSummaryIndices(settings, []); + expect(result).toBe(SLO_SUMMARY_DESTINATION_INDEX_PATTERN); + }); + + it('should return all remote clusters when enabled', function () { + const settings = { + useAllRemoteClusters: true, + selectedRemoteClusters: [], + }; + const clustersByName = [ + { name: 'cluster1', isConnected: true }, + { name: 'cluster2', isConnected: true }, + ]; + const result = getListOfSloSummaryIndices(settings, clustersByName); + expect(result).toBe( + `${SLO_SUMMARY_DESTINATION_INDEX_PATTERN},cluster1:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN},cluster2:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}` + ); + }); + + it('should return selected when enabled', function () { + const settings = { + useAllRemoteClusters: false, + selectedRemoteClusters: ['cluster1'], + }; + const clustersByName = [ + { name: 'cluster1', isConnected: true }, + { name: 'cluster2', isConnected: true }, + ]; + const result = getListOfSloSummaryIndices(settings, clustersByName); + expect(result).toBe( + `${SLO_SUMMARY_DESTINATION_INDEX_PATTERN},cluster1:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}` + ); + }); +}); diff --git a/x-pack/plugins/observability_solution/slo/common/summary_indices.ts b/x-pack/plugins/observability_solution/slo/common/summary_indices.ts new file mode 100644 index 0000000000000..c9a9d47bfdf1a --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/common/summary_indices.ts @@ -0,0 +1,28 @@ +/* + * 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 { GetSLOSettingsResponse } from '@kbn/slo-schema'; +import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from './constants'; + +export const getListOfSloSummaryIndices = ( + settings: GetSLOSettingsResponse, + clustersByName: Array<{ name: string; isConnected: boolean }> +) => { + const { useAllRemoteClusters, selectedRemoteClusters } = settings; + if (!useAllRemoteClusters && selectedRemoteClusters.length === 0) { + return SLO_SUMMARY_DESTINATION_INDEX_PATTERN; + } + + const indices: string[] = [SLO_SUMMARY_DESTINATION_INDEX_PATTERN]; + clustersByName.forEach(({ name, isConnected }) => { + if (isConnected && (useAllRemoteClusters || selectedRemoteClusters.includes(name))) { + indices.push(`${name}:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}`); + } + }); + + return indices.join(','); +}; diff --git a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/alert_time_table.tsx b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/alert_time_table.tsx index b2c582fc019ca..8960b96c443c9 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/alert_time_table.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/alert_time_table.tsx @@ -6,15 +6,15 @@ */ import { EuiBasicTable, EuiSpacer, EuiText, EuiTitle, HorizontalAlignment } from '@elastic/eui'; -import { SLOResponse } from '@kbn/slo-schema'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import { SLODefinitionResponse } from '@kbn/slo-schema'; +import React from 'react'; import { WindowSchema } from '../../typings'; import { toDuration, toMinutes } from '../../utils/slo/duration'; interface AlertTimeTableProps { - slo: SLOResponse; + slo: SLODefinitionResponse; windows: WindowSchema[]; } diff --git a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx index e414755d79585..c3768922b09e2 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx @@ -7,7 +7,7 @@ import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; import React, { useEffect, useState } from 'react'; -import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLODefinitionResponse } from '@kbn/slo-schema'; import { EuiCallOut, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -33,7 +33,7 @@ export function BurnRateRuleEditor(props: Props) { sloId: ruleParams?.sloId, }); - const [selectedSlo, setSelectedSlo] = useState(undefined); + const [selectedSlo, setSelectedSlo] = useState(undefined); const [windowDefs, setWindowDefs] = useState(ruleParams?.windows || []); const [dependencies, setDependencies] = useState(ruleParams?.dependencies || []); @@ -47,7 +47,7 @@ export function BurnRateRuleEditor(props: Props) { }); }, [initialSlo]); - const onSelectedSlo = (slo: SLOResponse | undefined) => { + const onSelectedSlo = (slo: SLODefinitionResponse | undefined) => { setSelectedSlo(slo); setWindowDefs(() => { return createDefaultWindows(slo); @@ -111,7 +111,7 @@ export function BurnRateRuleEditor(props: Props) { ); } -function createDefaultWindows(slo: SLOResponse | undefined) { +function createDefaultWindows(slo: SLODefinitionResponse | undefined) { const burnRateDefaults = slo ? BURN_RATE_DEFAULTS[slo.timeWindow.duration] : []; return burnRateDefaults.map((partialWindow) => createNewWindow(slo, partialWindow)); } diff --git a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.stories.tsx b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.stories.tsx index b69861b45103b..508bf924901ed 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.stories.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.stories.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import React from 'react'; -import { ComponentStory } from '@storybook/react'; -import { SLOResponse } from '@kbn/slo-schema'; - import { KibanaReactStorybookDecorator } from '@kbn/observability-plugin/public'; +import { SLODefinitionResponse } from '@kbn/slo-schema'; +import { ComponentStory } from '@storybook/react'; +import React from 'react'; import { SloSelector as Component } from './slo_selector'; export default { @@ -20,7 +19,7 @@ export default { const Template: ComponentStory = () => ( // eslint-disable-next-line no-console - console.log(slo)} /> + console.log(slo)} /> ); const defaultProps = {}; diff --git a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.tsx b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.tsx index b860502dc57a9..0d03903382509 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/slo_selector.tsx @@ -7,15 +7,15 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SLOResponse } from '@kbn/slo-schema'; +import { SLODefinitionResponse } from '@kbn/slo-schema'; import { debounce } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { useFetchSloDefinitions } from '../../hooks/use_fetch_slo_definitions'; interface Props { - initialSlo?: SLOResponse; + initialSlo?: SLODefinitionResponse; errors?: string[]; - onSelected: (slo: SLOResponse | undefined) => void; + onSelected: (slo: SLODefinitionResponse | undefined) => void; } function SloSelector({ initialSlo, onSelected, errors }: Props) { diff --git a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/windows.tsx b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/windows.tsx index ab01fee1b97ee..e0b960a7303da 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/windows.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/burn_rate_rule_editor/windows.tsx @@ -17,7 +17,7 @@ import { EuiTitle, EuiSwitch, } from '@elastic/eui'; -import { CreateSLOInput, SLOResponse } from '@kbn/slo-schema'; +import { CreateSLOInput, SLODefinitionResponse } from '@kbn/slo-schema'; import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { v4 } from 'uuid'; @@ -36,7 +36,7 @@ import { WindowResult } from './validation'; import { BudgetConsumed } from './budget_consumed'; interface WindowProps extends WindowSchema { - slo?: SLOResponse; + slo?: SLODefinitionResponse; onChange: (windowDef: WindowSchema) => void; onDelete: (id: string) => void; disableDelete: boolean; @@ -53,7 +53,7 @@ const ACTION_GROUP_OPTIONS = [ export const calculateMaxBurnRateThreshold = ( longWindow: Duration, - slo?: SLOResponse | CreateSLOInput + slo?: SLODefinitionResponse | CreateSLOInput ) => { return slo ? Math.floor(toMinutes(toDuration(slo.timeWindow.duration)) / toMinutes(longWindow)) @@ -246,7 +246,7 @@ const getErrorBudgetExhaustionText = ( }, }); export const createNewWindow = ( - slo?: SLOResponse | CreateSLOInput, + slo?: SLODefinitionResponse | CreateSLOInput, partialWindow: Partial = {} ): WindowSchema => { const longWindow = partialWindow.longWindow || { value: 1, unit: 'h' }; @@ -264,7 +264,7 @@ export const createNewWindow = ( interface WindowsProps { windows: WindowSchema[]; onChange: (windows: WindowSchema[]) => void; - slo?: SLOResponse; + slo?: SLODefinitionResponse; errors: WindowResult[]; totalNumberOfWindows?: number; } diff --git a/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx b/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx index 009c1ef31ff87..b37d70dfb4231 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks } from '@elast import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '../../utils/kibana_react'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { SLOS_BASE_PATH, SLO_SETTINGS_PATH } from '../../../common/locators/paths'; export function HeaderMenu(): React.ReactElement | null { const { http, theme } = useKibana().services; @@ -33,6 +34,15 @@ export function HeaderMenu(): React.ReactElement | null { defaultMessage: 'Add integrations', })} + + {i18n.translate('xpack.slo.headerMenu.settings', { + defaultMessage: 'Settings', + })} + diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rate.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rate.tsx index 3c630263fedb1..9d32524a2ce58 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rate.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rate.tsx @@ -8,13 +8,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat, EuiText, EuiTextColor } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import { SLOResponse } from '@kbn/slo-schema'; +import { SLODefinitionResponse } from '@kbn/slo-schema'; import moment from 'moment'; import React from 'react'; import { toDuration, toMinutes } from '../../../utils/slo/duration'; export interface BurnRateParams { - slo: SLOResponse; + slo: SLODefinitionResponse; threshold: number; burnRate?: number; isLoading?: boolean; @@ -40,7 +40,7 @@ function getTitleFromStatus(status: Status): string { function getSubtitleFromStatus( status: Status, burnRate: number | undefined = 1, - slo: SLOResponse + slo: SLODefinitionResponse ): string { if (status === 'NO_DATA') return i18n.translate('xpack.slo.burnRate.noDataStatusSubtitle', { diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx index d14a36e77f88f..df74bba743249 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx @@ -7,12 +7,12 @@ import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ALL_VALUE, SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLODefinitionResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import React from 'react'; import { getGroupKeysProse } from '../../../utils/slo/groupings'; export interface SloDeleteConfirmationModalProps { - slo: SLOWithSummaryResponse | SLOResponse; + slo: SLOWithSummaryResponse | SLODefinitionResponse; onCancel: () => void; onConfirm: () => void; } diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx index f5b1dd0e331cf..9353d1d574718 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx @@ -6,7 +6,7 @@ */ import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { SLOResponse } from '@kbn/slo-schema'; +import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; import React from 'react'; import { useKibana } from '../../../utils/kibana_react'; @@ -14,7 +14,7 @@ import { getDelayInSecondsFromSLO } from '../../../utils/slo/get_delay_in_second import { AlertAnnotation, TimeRange, useLensDefinition } from './use_lens_definition'; interface Props { - slo: SLOResponse; + slo: SLOWithSummaryResponse; dataTimeRange: TimeRange; threshold: number; alertTimeRange?: TimeRange; diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts index 1624418a1e986..a132076f8c2ed 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts @@ -9,7 +9,7 @@ import { transparentize, useEuiTheme } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; -import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../common/constants'; @@ -25,7 +25,7 @@ export interface AlertAnnotation { } export function useLensDefinition( - slo: SLOResponse, + slo: SLOWithSummaryResponse, threshold: number, alertTimeRange?: TimeRange, annotations?: AlertAnnotation[], @@ -466,7 +466,9 @@ export function useLensDefinition( adHocDataViews: { '32ca1ad4-81c0-4daf-b9d1-07118044bdc5': { id: '32ca1ad4-81c0-4daf-b9d1-07118044bdc5', - title: SLO_DESTINATION_INDEX_PATTERN, + title: !!slo.remote + ? `${slo.remote.remoteName}:${SLO_DESTINATION_INDEX_PATTERN}` + : SLO_DESTINATION_INDEX_PATTERN, timeFieldName: '@timestamp', sourceFilters: [], fieldFormats: {}, diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx index 96d62ddff8d49..14a925d5502e2 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx @@ -7,11 +7,11 @@ import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { SLODefinitionResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import React from 'react'; export interface SloResetConfirmationModalProps { - slo: SLOWithSummaryResponse | SLOResponse; + slo: SLOWithSummaryResponse | SLODefinitionResponse; onCancel: () => void; onConfirm: () => void; } diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_status_badge.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_status_badge.tsx index df38bdace3360..7e0de9274ddde 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_status_badge.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_status_badge.tsx @@ -33,25 +33,31 @@ export function SloStatusBadge({ slo }: SloStatusProps) { )} {slo.summary.status === 'HEALTHY' && ( - - {i18n.translate('xpack.slo.sloStatusBadge.healthy', { - defaultMessage: 'Healthy', - })} - +
+ + {i18n.translate('xpack.slo.sloStatusBadge.healthy', { + defaultMessage: 'Healthy', + })} + +
)} {slo.summary.status === 'DEGRADING' && ( - - {i18n.translate('xpack.slo.sloStatusBadge.degrading', { - defaultMessage: 'Degrading', - })} - +
+ + {i18n.translate('xpack.slo.sloStatusBadge.degrading', { + defaultMessage: 'Degrading', + })} + +
)} {slo.summary.status === 'VIOLATED' && ( - - {i18n.translate('xpack.slo.sloStatusBadge.violated', { - defaultMessage: 'Violated', - })} - +
+ + {i18n.translate('xpack.slo.sloStatusBadge.violated', { + defaultMessage: 'Violated', + })} + +
)} diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/slo_error_budget_burn_down.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/slo_error_budget_burn_down.tsx index a7cf47ab4758d..c176c83960f33 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/slo_error_budget_burn_down.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/slo_error_budget_burn_down.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiLink } from '@elastic/eu import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { useFetchSloList } from '../../../hooks/use_fetch_slo_list'; import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter'; import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary'; import { useFetchSloDetails } from '../../../hooks/use_fetch_slo_details'; @@ -40,9 +41,15 @@ export function SloErrorBudget({ }; }, [reloadSubject]); + const kqlQuery = `slo.id:"${sloId}" and slo.instanceId:"${sloInstanceId}"`; + + const { data: sloList } = useFetchSloList({ + kqlQuery, + }); + const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } = useFetchHistoricalSummary({ - list: [{ sloId: sloId!, instanceId: sloInstanceId ?? ALL_VALUE }], + sloList: sloList?.results ?? [], shouldRefetch: false, }); diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_configuration.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_configuration.tsx index 068ef4370b1f0..9f02c53775a8a 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_configuration.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_configuration.tsx @@ -64,6 +64,7 @@ function SingleSloConfiguration({ overviewMode, onCreate, onCancel }: SingleConf showAllGroupByInstances, sloId: selectedSlo?.sloId, sloInstanceId: selectedSlo?.sloInstanceId, + remoteName: selectedSlo?.remoteName, overviewMode, }); @@ -73,14 +74,18 @@ function SingleSloConfiguration({ overviewMode, onCreate, onCancel }: SingleConf - + { setHasError(slo === undefined); if (slo && 'id' in slo) { - setSelectedSlo({ sloId: slo.id, sloInstanceId: slo.instanceId }); + setSelectedSlo({ + sloId: slo.id, + sloInstanceId: slo.instanceId, + remoteName: slo.remote?.remoteName, + }); } }} /> diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable.tsx index 94faac22334d3..0a690c2310038 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable.tsx @@ -92,8 +92,14 @@ export class SLOEmbeddable extends AbstractEmbeddable ); } diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview.tsx index 7c85ee404b0f0..8feaeb19faf92 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview.tsx @@ -24,6 +24,7 @@ import { SingleSloProps } from './types'; export function SloOverview({ sloId, sloInstanceId, + remoteName, onRenderComplete, reloadSubject, }: SingleSloProps) { @@ -45,6 +46,7 @@ export function SloOverview({ isRefetching, } = useFetchSloDetails({ sloId, + remoteName, instanceId: sloInstanceId, }); @@ -57,7 +59,7 @@ export function SloOverview({ }); const { data: historicalSummaries = [] } = useFetchHistoricalSummary({ - list: slo ? [{ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE }] : [], + sloList: slo ? [slo] : [], }); const [selectedSlo, setSelectedSlo] = useState(null); diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx index abe30a84e5527..28eae14d18f81 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx @@ -96,12 +96,7 @@ export function SloCardChartList({ sloId }: { sloId: string }) { }); const { data: historicalSummaries = [] } = useFetchHistoricalSummary({ - list: [ - { - sloId, - instanceId: ALL_VALUE, - }, - ], + sloList: sloList?.results ?? [], }); const { colors } = useSloCardColor(); diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts index eeea74160c368..8cc0c7c5fb90d 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts @@ -7,7 +7,6 @@ import { EmbeddableInput } from '@kbn/embeddable-plugin/public'; import { Subject } from 'rxjs'; import { Filter } from '@kbn/es-query'; - export type OverviewMode = 'single' | 'groups'; export type GroupBy = 'slo.tags' | 'status' | 'slo.indicator.type'; export interface GroupFilters { @@ -28,6 +27,9 @@ export type GroupSloProps = EmbeddableSloProps & { }; export interface EmbeddableSloProps { + sloId?: string; + sloInstanceId?: string; + remoteName?: string; reloadSubject?: Subject; onRenderComplete?: () => void; overviewMode?: OverviewMode; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/__storybook_mocks__/use_fetch_historical_summary.ts b/x-pack/plugins/observability_solution/slo/public/hooks/__storybook_mocks__/use_fetch_historical_summary.ts index 9a33d728f3c3c..c31b8850e6d1e 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/__storybook_mocks__/use_fetch_historical_summary.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/__storybook_mocks__/use_fetch_historical_summary.ts @@ -10,13 +10,13 @@ import { HEALTHY_ROLLING_SLO, historicalSummaryData } from '../../data/slo/histo import { Params, UseFetchHistoricalSummaryResponse } from '../use_fetch_historical_summary'; export const useFetchHistoricalSummary = ({ - list = [], + sloList = [], }: Params): UseFetchHistoricalSummaryResponse => { const data: FetchHistoricalSummaryResponse = []; - list.forEach(({ sloId, instanceId }) => + sloList.forEach(({ id, instanceId }) => data.push({ - sloId, - instanceId, + sloId: id, + instanceId: instanceId!, data: historicalSummaryData.find((datum) => datum.sloId === HEALTHY_ROLLING_SLO)!.data, }) ); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/active_alerts.ts b/x-pack/plugins/observability_solution/slo/public/hooks/active_alerts.ts index 2a4b387df32a0..e2045839b1e89 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/active_alerts.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/active_alerts.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; -type SLO = Pick; +type SLO = Pick; export class ActiveAlerts { private data: Map = new Map(); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts index 076e6c0b001a3..dc7901e81b528 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts @@ -6,27 +6,31 @@ */ import { encode } from '@kbn/rison'; -import { useCallback } from 'react'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; -import { useKibana } from '../utils/kibana_react'; +import { useCallback } from 'react'; import { paths } from '../../common/locators/paths'; +import { useKibana } from '../utils/kibana_react'; +import { createRemoteSloCloneUrl } from '../utils/slo/remote_slo_urls'; +import { useSpace } from './use_space'; export function useCloneSlo() { const { http: { basePath }, application: { navigateToUrl }, } = useKibana().services; + const spaceId = useSpace(); return useCallback( (slo: SLOWithSummaryResponse) => { - navigateToUrl( - basePath.prepend( - paths.sloCreateWithEncodedForm( - encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined }) - ) - ) - ); + if (slo.remote) { + window.open(createRemoteSloCloneUrl(slo, spaceId), '_blank'); + } else { + const clonePath = paths.sloCreateWithEncodedForm( + encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined }) + ); + navigateToUrl(basePath.prepend(clonePath)); + } }, - [navigateToUrl, basePath] + [navigateToUrl, basePath, spaceId] ); } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts index 128f012094793..114d61b69a2b2 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts @@ -10,7 +10,7 @@ import { DataView } from '@kbn/data-views-plugin/common'; import { useKibana } from '../utils/kibana_react'; interface UseCreateDataViewProps { - indexPatternString: string | undefined; + indexPatternString?: string; } export function useCreateDataView({ indexPatternString }: UseCreateDataViewProps) { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts index e17c6a8f65557..0627b1b50c923 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts @@ -6,7 +6,7 @@ */ import { useQuery } from '@tanstack/react-query'; -import { FetchHistoricalSummaryResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, FetchHistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useKibana } from '../utils/kibana_react'; import { sloKeys } from './query_key_factory'; import { SLO_LONG_REFETCH_INTERVAL } from '../constants'; @@ -21,16 +21,27 @@ export interface UseFetchHistoricalSummaryResponse { } export interface Params { - list: Array<{ sloId: string; instanceId: string }>; + sloList: SLOWithSummaryResponse[]; shouldRefetch?: boolean; } export function useFetchHistoricalSummary({ - list = [], + sloList = [], shouldRefetch, }: Params): UseFetchHistoricalSummaryResponse { const { http } = useKibana().services; + const list = sloList.map((slo) => ({ + sloId: slo.id, + instanceId: slo.instanceId ?? ALL_VALUE, + remoteName: slo.remote?.remoteName, + timeWindow: slo.timeWindow, + groupBy: slo.groupBy, + revision: slo.revision, + objective: slo.objective, + budgetingMethod: slo.budgetingMethod, + })); + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ queryKey: sloKeys.historicalSummary(list), queryFn: async ({ signal }) => { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts index 30793619dd255..1623e6609b85b 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts @@ -47,7 +47,11 @@ export function useFetchSloBurnRates({ const response = await http.post( `/internal/observability/slos/${slo.id}/_burn_rates`, { - body: JSON.stringify({ windows, instanceId: slo.instanceId ?? ALL_VALUE }), + body: JSON.stringify({ + windows, + instanceId: slo.instanceId ?? ALL_VALUE, + remoteName: slo.remote?.remoteName, + }), signal, } ); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts index d835a94e77031..9f5e2ef718182 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts @@ -31,10 +31,12 @@ export interface UseFetchSloDetailsResponse { export function useFetchSloDetails({ sloId, instanceId, + remoteName, shouldRefetch, }: { sloId?: string; instanceId?: string; + remoteName?: string | null; shouldRefetch?: boolean; }): UseFetchSloDetailsResponse { const { http } = useKibana().services; @@ -47,6 +49,7 @@ export function useFetchSloDetails({ const response = await http.get(`/api/observability/slos/${sloId}`, { query: { ...(!!instanceId && instanceId !== ALL_VALUE && { instanceId }), + ...(remoteName && { remoteName }), }, signal, }); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts index 983097a5eb721..b868768d45c72 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts @@ -6,12 +6,12 @@ */ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { CreateSLOInput, SLOResponse } from '@kbn/slo-schema'; +import type { CreateSLOInput, SLODefinitionResponse } from '@kbn/slo-schema'; import { useQuery } from '@tanstack/react-query'; import { useKibana } from '../utils/kibana_react'; interface SLOInspectResponse { - slo: SLOResponse; + slo: SLODefinitionResponse; pipeline: Record; rollUpTransform: TransformPutTransformRequest; summaryTransform: TransformPutTransformRequest; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts index 427db4546ee6c..fc032e96cf9cc 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts @@ -26,10 +26,12 @@ export function useGetPreviewData({ groupBy, groupings, instanceId, + remoteName, }: { isValid: boolean; groupBy?: string; instanceId?: string; + remoteName?: string; groupings?: Record; objective?: Objective; indicator: Indicator; @@ -49,6 +51,7 @@ export function useGetPreviewData({ groupBy, instanceId, groupings, + remoteName, ...(objective ? { objective } : null), }), signal, diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts new file mode 100644 index 0000000000000..c52056f006de2 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts @@ -0,0 +1,22 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useKibana } from '../utils/kibana_react'; + +export function useSpace() { + const { spaces } = useKibana().services; + const [spaceId, setSpaceId] = useState(); + + useEffect(() => { + if (spaces) { + spaces.getActiveSpace().then((space) => setSpaceId(space.id)); + } + }, [spaces]); + + return spaceId; +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx index 764dcda8e04b3..8974b1fabae12 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx @@ -65,6 +65,7 @@ export function EventsChartPanel({ slo, range }: Props) { indicator: slo.indicator, groupings: slo.groupings, instanceId: slo.instanceId, + remoteName: slo.remote?.remoteName, }); const dateFormat = uiSettings.get('dateFormat'); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx index a84e1082050a9..ddf350c0df478 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx @@ -5,25 +5,30 @@ * 2.0. */ -import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiIcon, + EuiPopover, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SLOWithSummaryResponse } from '@kbn/slo-schema'; -import React, { useCallback, useState } from 'react'; - -import type { RulesParams } from '@kbn/observability-plugin/public'; -import { rulesLocatorID } from '@kbn/observability-plugin/common'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { sloFeatureId } from '@kbn/observability-plugin/common'; -import { EditBurnRateRuleFlyout } from '../../slos/components/common/edit_burn_rate_rule_flyout'; -import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo'; -import { useKibana } from '../../../utils/kibana_react'; +import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import React, { useCallback, useEffect, useState } from 'react'; import { paths } from '../../../../common/locators/paths'; import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; import { useCapabilities } from '../../../hooks/use_capabilities'; import { useCloneSlo } from '../../../hooks/use_clone_slo'; import { useDeleteSlo } from '../../../hooks/use_delete_slo'; +import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo'; +import { useKibana } from '../../../utils/kibana_react'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { isApmIndicatorType } from '../../../utils/slo/indicator'; +import { EditBurnRateRuleFlyout } from '../../slos/components/common/edit_burn_rate_rule_flyout'; +import { useGetQueryParams } from '../hooks/use_get_query_params'; +import { useSloActions } from '../hooks/use_slo_actions'; export interface Props { slo?: SLOWithSummaryResponse; @@ -34,18 +39,17 @@ export function HeaderControl({ isLoading, slo }: Props) { const { application: { navigateToUrl, capabilities }, http: { basePath }, - share: { - url: { locators }, - }, triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, } = useKibana().services; + const hasApmReadCapabilities = capabilities.apm.show; const { hasWriteCapabilities } = useCapabilities(); + const { isDeletingSlo, removeDeleteQueryParam } = useGetQueryParams(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [isEditRuleFlyoutOpen, setIsEditRuleFlyoutOpen] = useState(false); - const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); const { mutate: deleteSlo } = useDeleteSlo(); @@ -59,11 +63,11 @@ export function HeaderControl({ isLoading, slo }: Props) { const handleActionsClick = () => setIsPopoverOpen((value) => !value); const closePopover = () => setIsPopoverOpen(false); - const handleEdit = () => { - if (slo) { - navigate(basePath.prepend(paths.sloEdit(slo.id))); + useEffect(() => { + if (isDeletingSlo) { + setDeleteConfirmationModalOpen(true); } - }; + }, [isDeletingSlo]); const onCloseRuleFlyout = () => { setRuleFlyoutVisibility(false); @@ -74,18 +78,12 @@ export function HeaderControl({ isLoading, slo }: Props) { setRuleFlyoutVisibility(true); }; - const handleNavigateToRules = async () => { - if (rules.length === 1) { - setIsEditRuleFlyoutOpen(true); - setIsPopoverOpen(false); - } else { - const locator = locators.get(rulesLocatorID); - - if (slo?.id && locator) { - locator.navigate({ params: { sloId: slo.id } }, { replace: false }); - } - } - }; + const { handleNavigateToRules, sloEditUrl, remoteDeleteUrl } = useSloActions({ + slo, + rules, + setIsEditRuleFlyoutOpen, + setIsActionsPopoverOpen: setIsPopoverOpen, + }); const handleNavigateToApm = () => { if (!slo) { @@ -108,11 +106,16 @@ export function HeaderControl({ isLoading, slo }: Props) { }; const handleDelete = () => { - setDeleteConfirmationModalOpen(true); - setIsPopoverOpen(false); + if (!!remoteDeleteUrl) { + window.open(remoteDeleteUrl, '_blank'); + } else { + setDeleteConfirmationModalOpen(true); + setIsPopoverOpen(false); + } }; const handleDeleteCancel = () => { + removeDeleteQueryParam(); setDeleteConfirmationModalOpen(false); }; @@ -128,6 +131,19 @@ export function HeaderControl({ isLoading, slo }: Props) { [navigateToUrl] ); + const isRemote = !!slo?.remote; + const hasUndefinedRemoteKibanaUrl = !!slo?.remote && slo?.remote?.kibanaUrl === ''; + + const showRemoteLinkIcon = isRemote ? ( + + ) : null; + return ( <> {i18n.translate('xpack.slo.sloDetails.headerControl.actions', { defaultMessage: 'Actions', @@ -155,21 +172,27 @@ export function HeaderControl({ isLoading, slo }: Props) { items={[ {i18n.translate('xpack.slo.sloDetails.headerControl.edit', { defaultMessage: 'Edit', })} + {showRemoteLinkIcon} , {i18n.translate('xpack.slo.sloDetails.headerControl.createBurnRateRule', { defaultMessage: 'Create new alert rule', @@ -177,15 +200,19 @@ export function HeaderControl({ isLoading, slo }: Props) { , {i18n.translate('xpack.slo.sloDetails.headerControl.manageRules', { defaultMessage: 'Manage burn rate {count, plural, one {rule} other {rules}}', values: { count: rules.length }, })} + {showRemoteLinkIcon} , ] .concat( @@ -193,9 +220,10 @@ export function HeaderControl({ isLoading, slo }: Props) { {i18n.translate('xpack.slo.sloDetails.headerControl.exploreInApm', { defaultMessage: 'Service details', @@ -208,25 +236,33 @@ export function HeaderControl({ isLoading, slo }: Props) { .concat( {i18n.translate('xpack.slo.slo.item.actions.clone', { defaultMessage: 'Clone', })} + {showRemoteLinkIcon} , {i18n.translate('xpack.slo.slo.item.actions.delete', { defaultMessage: 'Delete', })} + {showRemoteLinkIcon} )} /> @@ -259,3 +295,14 @@ export function HeaderControl({ isLoading, slo }: Props) { ); } + +const NOT_AVAILABLE_FOR_REMOTE = i18n.translate('xpack.slo.item.actions.notAvailable', { + defaultMessage: 'This action is not available for remote SLOs', +}); + +const NOT_AVAILABLE_FOR_UNDEFINED_REMOTE_KIBANA_URL = i18n.translate( + 'xpack.slo.item.actions.remoteKibanaUrlUndefined', + { + defaultMessage: 'This action is not available for remote SLOs with undefined kibanaUrl', + } +); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_title.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_title.tsx index df73290a09974..e9150e09cf1b0 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_title.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_title.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; import React from 'react'; +import { SloRemoteBadge } from '../../slos/components/badges/slo_remote_badge'; import { SLOGroupings } from '../../slos/components/common/slo_groupings'; import { SloStatusBadge } from '../../../components/slo/slo_status_badge'; @@ -36,6 +37,7 @@ export function HeaderTitle({ isLoading, slo }: Props) { wrap={true} > + @@ -59,6 +61,7 @@ export function HeaderTitle({ isLoading, slo }: Props) { + ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx index e44df33b42b2d..7a2be1bfb394e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import React, { useEffect, useState } from 'react'; import { BurnRateOption, BurnRates } from '../../../components/slo/burn_rate/burn_rates'; - import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary'; import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo'; import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter'; @@ -19,6 +18,7 @@ import { EventsChartPanel } from './events_chart_panel'; import { Overview } from './overview/overview'; import { SliChartPanel } from './sli_chart_panel'; import { SloDetailsAlerts } from './slo_detail_alerts'; +import { SloRemoteCallout } from './slo_remote_callout'; export const TAB_ID_URL_PARAM = 'tabId'; export const OVERVIEW_TAB_ID = 'overview'; @@ -91,7 +91,7 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) { const { data: historicalSummaries = [], isLoading: historicalSummaryLoading } = useFetchHistoricalSummary({ - list: [{ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE }], + sloList: [slo], shouldRefetch: isAutoRefreshing, }); @@ -125,6 +125,7 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) { return selectedTabId === OVERVIEW_TAB_ID ? ( + diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_remote_callout.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_remote_callout.tsx new file mode 100644 index 0000000000000..c71f956403188 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_remote_callout.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { createRemoteSloDetailsUrl } from '../../../utils/slo/remote_slo_urls'; +import { useSpace } from '../../../hooks/use_space'; + +export function SloRemoteCallout({ slo }: { slo: SLOWithSummaryResponse }) { + const spaceId = useSpace(); + const sloDetailsUrl = createRemoteSloDetailsUrl(slo, spaceId); + + if (!slo.remote) { + return null; + } + + return ( + +

+ {slo.remote.remoteName}, + kibanaUrl: ( + + {slo.remote.kibanaUrl} + + ), + }} + /> +

+ + {i18n.translate('xpack.slo.headerTitle.linkButtonButtonLabel', { + defaultMessage: 'View remote SLO details', + })} + + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_instance_id_query_param.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_instance_id_query_param.ts deleted file mode 100644 index ae848292da7c3..0000000000000 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_instance_id_query_param.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ALL_VALUE } from '@kbn/slo-schema'; -import { useLocation } from 'react-router-dom'; - -export const INSTANCE_SEARCH_PARAM = 'instanceId'; - -export function useGetInstanceIdQueryParam(): string | undefined { - const { search } = useLocation(); - const searchParams = new URLSearchParams(search); - - const instanceId = searchParams.get(INSTANCE_SEARCH_PARAM); - - return !!instanceId && instanceId !== ALL_VALUE ? instanceId : undefined; -} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts new file mode 100644 index 0000000000000..8e169e894c03f --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALL_VALUE } from '@kbn/slo-schema'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useCallback } from 'react'; + +export const INSTANCE_SEARCH_PARAM = 'instanceId'; +export const REMOTE_NAME_PARAM = 'remoteName'; +export const DELETE_SLO = 'delete'; + +export function useGetQueryParams() { + const { search, pathname } = useLocation(); + const history = useHistory(); + const searchParams = new URLSearchParams(search); + + const instanceId = searchParams.get(INSTANCE_SEARCH_PARAM); + const remoteName = searchParams.get(REMOTE_NAME_PARAM); + const deleteSlo = searchParams.get(DELETE_SLO); + + const removeDeleteQueryParam = useCallback(() => { + const qParams = new URLSearchParams(search); + + // remote delete param from url after initial load + if (deleteSlo === 'true') { + qParams.delete(DELETE_SLO); + history.replace({ + pathname, + search: qParams.toString(), + }); + } + }, [deleteSlo, history, pathname, search]); + + return { + instanceId: !!instanceId && instanceId !== ALL_VALUE ? instanceId : undefined, + remoteName, + isDeletingSlo: deleteSlo === 'true', + removeDeleteQueryParam, + }; +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts new file mode 100644 index 0000000000000..2129f0947361a --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts @@ -0,0 +1,90 @@ +/* + * 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 { rulesLocatorID, RulesParams } from '@kbn/observability-plugin/public'; +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import path from 'path'; +import { paths } from '../../../../common/locators/paths'; +import { useSpace } from '../../../hooks/use_space'; +import { BurnRateRuleParams } from '../../../typings'; +import { useKibana } from '../../../utils/kibana_react'; +import { + createRemoteSloDeleteUrl, + createRemoteSloEditUrl, +} from '../../../utils/slo/remote_slo_urls'; + +export const useSloActions = ({ + slo, + rules, + setIsEditRuleFlyoutOpen, + setIsActionsPopoverOpen, +}: { + slo?: SLOWithSummaryResponse; + rules?: Array>; + setIsEditRuleFlyoutOpen: (val: boolean) => void; + setIsActionsPopoverOpen: (val: boolean) => void; +}) => { + const { + share: { + url: { locators }, + }, + http, + } = useKibana().services; + const spaceId = useSpace(); + + if (!slo) { + return { + sloEditUrl: '', + handleNavigateToRules: () => {}, + remoteDeleteUrl: undefined, + sloDetailsUrl: '', + }; + } + + const handleNavigateToRules = async () => { + if (rules?.length === 1) { + // if there is only one rule we can edit inline in flyout + setIsEditRuleFlyoutOpen(true); + setIsActionsPopoverOpen(false); + } else { + const locator = locators.get(rulesLocatorID); + if (!locator) return undefined; + + if (slo.remote && slo.remote.kibanaUrl !== '') { + const basePath = http.basePath.get(); // "/kibana/s/my-space" + const url = await locator.getUrl({ params: { sloId: slo.id } }); // "/kibana/s/my-space/app/rules/123" + // since basePath is already included in the locatorUrl, we need to remove it from the start of url + const urlWithoutBasePath = url?.replace(basePath, ''); // "/app/rules/123" + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const remoteUrl = new URL(path.join(spacePath, urlWithoutBasePath), slo.remote.kibanaUrl); // "kibanaUrl/s/my-space/app/rules/123" + window.open(remoteUrl, '_blank'); + } else { + locator.navigate({ params: { sloId: slo.id } }, { replace: false }); + } + } + }; + + const detailsUrl = paths.sloDetails( + slo.id, + ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined, + slo.remote?.remoteName + ); + + const remoteDeleteUrl = createRemoteSloDeleteUrl(slo, spaceId); + + const sloEditUrl = slo.remote + ? createRemoteSloEditUrl(slo, spaceId) + : http.basePath.prepend(paths.sloEdit(slo.id)); + + return { + sloEditUrl, + handleNavigateToRules, + remoteDeleteUrl, + sloDetailsUrl: http.basePath.prepend(detailsUrl), + }; +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx index b49446f1651a6..5926d4f2cf406 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { EuiNotificationBadge } from '@elastic/eui'; +import { EuiNotificationBadge, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useFetchActiveAlerts } from '../../../hooks/use_fetch_active_alerts'; @@ -18,8 +18,8 @@ export const useSloDetailsTabs = ({ selectedTabId, setSelectedTabId, }: { - isAutoRefreshing: boolean; slo?: SLOWithSummaryResponse | null; + isAutoRefreshing: boolean; selectedTabId: SloTabId; setSelectedTabId: (val: SloTabId) => void; }) => { @@ -28,6 +28,8 @@ export const useSloDetailsTabs = ({ shouldRefetch: isAutoRefreshing, }); + const isRemote = !!slo?.remote; + const tabs = [ { id: OVERVIEW_TAB_ID, @@ -40,19 +42,34 @@ export const useSloDetailsTabs = ({ }, { id: ALERTS_TAB_ID, - label: i18n.translate('xpack.slo.sloDetails.tab.alertsLabel', { - defaultMessage: 'Alerts', - }), + label: isRemote ? ( + + <>{ALERTS_LABEL} + + ) : ( + ALERTS_LABEL + ), 'data-test-subj': 'alertsTab', + disabled: Boolean(isRemote), isSelected: selectedTabId === ALERTS_TAB_ID, - append: slo ? ( - - {(activeAlerts && activeAlerts.get(slo)) ?? 0} - - ) : null, + append: + slo && !isRemote ? ( + + {(activeAlerts && activeAlerts.get(slo)) ?? 0} + + ) : null, onClick: () => setSelectedTabId(ALERTS_TAB_ID), }, ]; return { tabs }; }; + +const ALERTS_LABEL = i18n.translate('xpack.slo.sloDetails.tab.alertsLabel', { + defaultMessage: 'Alerts', +}); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx index 457ee6deeb75c..1fddae776ec01 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx @@ -35,7 +35,7 @@ import { HeaderControl } from './components/header_control'; import { paths } from '../../../common/locators/paths'; import type { SloDetailsPathParams } from './types'; import { AutoRefreshButton } from '../../components/slo/auto_refresh_button'; -import { useGetInstanceIdQueryParam } from './hooks/use_get_instance_id_query_param'; +import { useGetQueryParams } from './hooks/use_get_query_params'; import { useAutoRefreshStorage } from '../../components/slo/auto_refresh_button/hooks/use_auto_refresh_storage'; export function SloDetailsPage() { @@ -50,11 +50,12 @@ export function SloDetailsPage() { const hasRightLicense = hasAtLeast('platinum'); const { sloId } = useParams(); - const sloInstanceId = useGetInstanceIdQueryParam(); + const { instanceId: sloInstanceId, remoteName } = useGetQueryParams(); const { storeAutoRefreshState, getAutoRefreshState } = useAutoRefreshStorage(); const [isAutoRefreshing, setIsAutoRefreshing] = useState(getAutoRefreshState()); const { isLoading, data: slo } = useFetchSloDetails({ sloId, + remoteName, instanceId: sloInstanceId, shouldRefetch: isAutoRefreshing, }); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_objective_section.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_objective_section.tsx index f1a8ee3aa03b5..ebb8250493748 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_objective_section.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_objective_section.tsx @@ -18,7 +18,7 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { TimeWindow } from '@kbn/slo-schema'; +import { TimeWindowType } from '@kbn/slo-schema'; import React, { useEffect, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -46,7 +46,7 @@ export function SloEditFormObjectiveSection() { const timeWindowType = watch('timeWindow.type'); const indicator = watch('indicator.type'); - const [timeWindowTypeState, setTimeWindowTypeState] = useState( + const [timeWindowTypeState, setTimeWindowTypeState] = useState( defaultValues?.timeWindow?.type ); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/constants.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/constants.ts index 513197e291b59..2c79f9736fce7 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/constants.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/constants.ts @@ -17,7 +17,7 @@ import { KQLCustomIndicator, MetricCustomIndicator, TimesliceMetricIndicator, - TimeWindow, + TimeWindowType, } from '@kbn/slo-schema'; import { BUDGETING_METHOD_OCCURRENCES, @@ -78,7 +78,7 @@ export const BUDGETING_METHOD_OPTIONS: Array<{ value: BudgetingMethod; text: str }, ]; -export const TIMEWINDOW_TYPE_OPTIONS: Array<{ value: TimeWindow; text: string }> = [ +export const TIMEWINDOW_TYPE_OPTIONS: Array<{ value: TimeWindowType; text: string }> = [ { value: 'rolling', text: i18n.translate('xpack.slo.sloEdit.timeWindow.rolling', { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/types.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/types.ts index 4a85822f84ea3..d876e3a276208 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/types.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BudgetingMethod, Indicator, TimeWindow } from '@kbn/slo-schema'; +import { BudgetingMethod, Indicator, TimeWindowType } from '@kbn/slo-schema'; export interface CreateSLOForm { name: string; @@ -13,7 +13,7 @@ export interface CreateSLOForm { indicator: IndicatorType; timeWindow: { duration: string; - type: TimeWindow; + type: TimeWindowType; }; tags: string[]; budgetingMethod: BudgetingMethod; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx index ad38471a15095..30e5c06cfbc07 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx @@ -4,19 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiButton } from '@elastic/eui'; -import { SLOResponse } from '@kbn/slo-schema'; +import { SLODefinitionResponse } from '@kbn/slo-schema'; +import React, { useState } from 'react'; import { SloDeleteConfirmationModal } from '../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; -import { SloTimeWindowBadge } from '../slos/components/badges/slo_time_window_badge'; -import { SloIndicatorTypeBadge } from '../slos/components/badges/slo_indicator_type_badge'; +import { SloResetConfirmationModal } from '../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal'; import { useDeleteSlo } from '../../hooks/use_delete_slo'; import { useResetSlo } from '../../hooks/use_reset_slo'; -import { SloResetConfirmationModal } from '../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal'; +import { SloIndicatorTypeBadge } from '../slos/components/badges/slo_indicator_type_badge'; +import { SloTimeWindowBadge } from '../slos/components/badges/slo_time_window_badge'; interface OutdatedSloProps { - slo: SLOResponse; + slo: SLODefinitionResponse; onReset: () => void; onDelete: () => void; } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx new file mode 100644 index 0000000000000..86a2597c2188a --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +import { + EuiForm, + EuiFormRow, + EuiSwitch, + EuiDescribedFormGroup, + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { isEqual } from 'lodash'; +import { useGetSettings } from './use_get_settings'; +import { usePutSloSettings } from './use_put_slo_settings'; + +export function SettingsForm() { + const [useAllRemoteClusters, setUseAllRemoteClusters] = useState(false); + const [selectedRemoteClusters, setSelectedRemoteClusters] = useState([]); + + const { http } = useKibana().services; + + const { data: currentSettings } = useGetSettings(); + const { mutateAsync: updateSettings } = usePutSloSettings(); + + const { data, loading } = useFetcher(() => { + return http?.get>('/api/remote_clusters'); + }, [http]); + + useEffect(() => { + if (currentSettings) { + setUseAllRemoteClusters(currentSettings.useAllRemoteClusters); + setSelectedRemoteClusters(currentSettings.selectedRemoteClusters); + } + }, [currentSettings]); + + const onSubmit = async () => { + updateSettings({ + settings: { + useAllRemoteClusters, + selectedRemoteClusters, + }, + }); + }; + + return ( + + + {i18n.translate('xpack.slo.settingsForm.h3.sourceSettingsLabel', { + defaultMessage: 'Source settings', + })} + + } + description={ +

+ {i18n.translate('xpack.slo.settingsForm.p.fetchSlosFromAllLabel', { + defaultMessage: 'Fetch SLOs from all remote clusters.', + })} +

+ } + > + + setUseAllRemoteClusters(evt.target.checked)} + /> + +
+ + {i18n.translate('xpack.slo.settingsForm.h3.remoteSettingsLabel', { + defaultMessage: 'Remote clusters', + })} + + } + description={ +

+ {i18n.translate('xpack.slo.settingsForm.select.fetchSlosFromAllLabel', { + defaultMessage: 'Select remote clusters to fetch SLOs from.', + })} +

+ } + > + + ({ label: cluster.name, value: cluster.name })) || []} + selectedOptions={selectedRemoteClusters.map((cluster) => ({ + label: cluster, + value: cluster, + }))} + onChange={(sels) => { + setSelectedRemoteClusters(sels.map((s) => s.value as string)); + }} + isDisabled={useAllRemoteClusters} + /> + + + + + { + setUseAllRemoteClusters(currentSettings?.useAllRemoteClusters || false); + setSelectedRemoteClusters(currentSettings?.selectedRemoteClusters || []); + }} + isDisabled={isEqual(currentSettings, { + useAllRemoteClusters, + selectedRemoteClusters, + })} + > + {i18n.translate('xpack.slo.settingsForm.euiButtonEmpty.cancelLabel', { + defaultMessage: 'Cancel', + })} + + + + onSubmit()} + isDisabled={isEqual(currentSettings, { + useAllRemoteClusters, + selectedRemoteClusters, + })} + > + {i18n.translate('xpack.slo.settingsForm.applyButtonEmptyLabel', { + defaultMessage: 'Apply', + })} + + + +
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx new file mode 100644 index 0000000000000..d2a2da4a5fafa --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { SettingsForm } from './settings_form'; +import { useKibana } from '../../utils/kibana_react'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { paths } from '../../../common/locators/paths'; +import { HeaderMenu } from '../../components/header_menu/header_menu'; + +export function SloSettingsPage() { + const { + http: { basePath }, + } = useKibana().services; + const { ObservabilityPageTemplate } = usePluginContext(); + + useBreadcrumbs([ + { + href: basePath.prepend(paths.slosSettings), + text: i18n.translate('xpack.slo.breadcrumbs.slosSettingsText', { + defaultMessage: 'SLOs Settings', + }), + }, + ]); + + return ( + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_get_settings.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_get_settings.ts new file mode 100644 index 0000000000000..aa30659d85d3b --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_get_settings.ts @@ -0,0 +1,33 @@ +/* + * 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 { GetSLOSettingsResponse } from '@kbn/slo-schema'; +import { useQuery } from '@tanstack/react-query'; +import { useKibana } from '../../utils/kibana_react'; + +export const useGetSettings = () => { + const { http } = useKibana().services; + const { isLoading, data } = useQuery({ + queryKey: ['getSloSettings'], + queryFn: async ({ signal }) => { + try { + return http.get('/internal/slo/settings', { signal }); + } catch (error) { + return defaultSettings; + } + }, + keepPreviousData: true, + refetchOnWindowFocus: false, + }); + + return { isLoading, data }; +}; + +const defaultSettings: GetSLOSettingsResponse = { + useAllRemoteClusters: false, + selectedRemoteClusters: [], +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_put_slo_settings.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_put_slo_settings.tsx new file mode 100644 index 0000000000000..48c9a54eea295 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_put_slo_settings.tsx @@ -0,0 +1,48 @@ +/* + * 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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { PutSLOSettingsParams, PutSLOSettingsResponse } from '@kbn/slo-schema'; +import { useMutation } from '@tanstack/react-query'; +import { paths } from '../../../common/locators/paths'; +import { useKibana } from '../../utils/kibana_react'; + +type ServerError = IHttpFetchError; + +export function usePutSloSettings() { + const { + application: { navigateToUrl }, + http, + notifications: { toasts }, + } = useKibana().services; + + return useMutation( + ['putSloSettings'], + ({ settings }) => { + const body = JSON.stringify(settings); + return http.put(`/internal/slo/settings`, { body }); + }, + { + onSuccess: (data, { settings }) => { + toasts.addSuccess({ + title: i18n.translate('xpack.slo.settings.successNotification', { + defaultMessage: 'Success updated slo settings', + }), + }); + navigateToUrl(http.basePath.prepend(paths.slos)); + }, + onError: (error, { settings }, context) => { + toasts.addError(new Error(error.body?.message ?? error.message), { + title: i18n.translate('xpack.slo.settings.errorNotification', { + defaultMessage: 'Something went wrong while updating settings', + }), + }); + }, + } + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_badges.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_badges.tsx index fc3693c01a561..86dfa006b7f3c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_badges.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_badges.tsx @@ -14,6 +14,7 @@ import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badg import { BurnRateRuleParams } from '../../../../typings'; import { SloTagsList } from '../common/slo_tags_list'; import { SloIndicatorTypeBadge } from './slo_indicator_type_badge'; +import { SloRemoteBadge } from './slo_remote_badge'; import { SloRulesBadge } from './slo_rules_badge'; import { SloTimeWindowBadge } from './slo_time_window_badge'; @@ -44,7 +45,8 @@ export function SloBadges({ - + + )} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx index 727c3af91e5f5..a721499e9ef01 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx @@ -10,21 +10,21 @@ import { i18n } from '@kbn/i18n'; import { apmTransactionDurationIndicatorSchema, apmTransactionErrorRateIndicatorSchema, - SLOResponse, + SLODefinitionResponse, SLOWithSummaryResponse, } from '@kbn/slo-schema'; import { euiLightVars } from '@kbn/ui-theme'; import React, { MouseEvent } from 'react'; import { useRouteMatch } from 'react-router-dom'; import { SLOS_PATH } from '../../../../../common/locators/paths'; -import { useUrlSearchState } from '../../hooks/use_url_search_state'; import { useKibana } from '../../../../utils/kibana_react'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { toIndicatorTypeLabel } from '../../../../utils/slo/labels'; +import { useUrlSearchState } from '../../hooks/use_url_search_state'; export interface Props { color?: EuiBadgeProps['color']; - slo: SLOWithSummaryResponse | SLOResponse; + slo: SLOWithSummaryResponse | SLODefinitionResponse; } export function SloIndicatorTypeBadge({ slo, color }: Props) { @@ -49,7 +49,7 @@ export function SloIndicatorTypeBadge({ slo, color }: Props) { { + onClick={(_) => { if (isSloPage) { onStateChange({ kqlQuery: `slo.indicator.type: ${slo.indicator.type}`, @@ -68,31 +68,32 @@ export function SloIndicatorTypeBadge({ slo, color }: Props) { {(apmTransactionDurationIndicatorSchema.is(slo.indicator) || - apmTransactionErrorRateIndicatorSchema.is(slo.indicator)) && ( - - - ) => { - e.stopPropagation(); // stops propagation of metric onElementClick - }} - onClickAriaLabel={i18n.translate('xpack.slo.indicatorTypeBadge.exploreInApm', { + apmTransactionErrorRateIndicatorSchema.is(slo.indicator)) && + slo.indicator.params.service !== '' && ( + + - {slo.indicator.params.service} - - - - )} + ) => { + e.stopPropagation(); // stops propagation of metric onElementClick + }} + onClickAriaLabel={i18n.translate('xpack.slo.indicatorTypeBadge.exploreInApm', { + defaultMessage: 'View {service} details', + values: { service: slo.indicator.params.service }, + })} + > + {slo.indicator.params.service} + + + + )} ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_remote_badge.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_remote_badge.tsx new file mode 100644 index 0000000000000..245a16e9c793a --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_remote_badge.tsx @@ -0,0 +1,40 @@ +/* + * 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 { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import React, { MouseEvent } from 'react'; +import { useSpace } from '../../../../hooks/use_space'; +import { createRemoteSloDetailsUrl } from '../../../../utils/slo/remote_slo_urls'; + +export function SloRemoteBadge({ slo }: { slo: SLOWithSummaryResponse }) { + const spaceId = useSpace(); + if (!slo.remote) { + return null; + } + + const sloDetailsUrl = createRemoteSloDetailsUrl(slo, spaceId); + return ( + + + { + e.stopPropagation(); + }} + > + {i18n.translate('xpack.slo.sloCardItemBadges.remoteBadgeLabel', { + defaultMessage: 'Remote', + })} + + + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_rules_badge.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_rules_badge.tsx index 9a275aad343c5..d58f33d864f74 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_rules_badge.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_rules_badge.tsx @@ -13,10 +13,15 @@ import { BurnRateRuleParams } from '../../../../typings'; export interface Props { rules: Array> | undefined; + isRemote?: boolean; onClick?: () => void; } -export function SloRulesBadge({ rules, onClick }: Props) { +export function SloRulesBadge({ rules, onClick, isRemote }: Props) { + if (isRemote) { + return null; + } + return rules === undefined || rules.length > 0 ? null : ( { @@ -56,8 +58,9 @@ export function SloCardItemBadges({ slo, activeAlerts, rules, handleCreateRule } <> - + + ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })), + sloList, }); const columns = useColumns(); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/quick_filters.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/quick_filters.tsx index dc9d6065fe12e..c865925c67c3d 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/quick_filters.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/quick_filters.tsx @@ -9,23 +9,24 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/common'; +import { DataView } from '@kbn/data-views-plugin/common'; import styled from 'styled-components'; import { Filter } from '@kbn/es-query'; import { isEmpty } from 'lodash'; -import { useCreateDataView } from '../../../../hooks/use_create_data_view'; import { SearchState } from '../../hooks/use_url_search_state'; -import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../../../common/constants'; interface Props { initialState: SearchState; loading: boolean; + dataView?: DataView; onStateChange: (newState: Partial) => void; } -export function QuickFilters({ initialState: { tagsFilter, statusFilter }, onStateChange }: Props) { - const { dataView, loading } = useCreateDataView({ - indexPatternString: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - }); +export function QuickFilters({ + dataView, + initialState: { tagsFilter, statusFilter }, + onStateChange, +}: Props) { const [controlGroupAPI, setControlGroupAPI] = useState(); useEffect(() => { @@ -47,7 +48,7 @@ export function QuickFilters({ initialState: { tagsFilter, statusFilter }, onSta }; }, [controlGroupAPI, onStateChange]); - if (loading || !dataView) { + if (!dataView) { return null; } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/sort_by_select.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/sort_by_select.tsx index 214dad4e9a124..3d977a19cb0a8 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/sort_by_select.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/sort_by_select.tsx @@ -9,10 +9,9 @@ import { EuiPanel, EuiSelectableOption, EuiText } from '@elastic/eui'; import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import type { SearchState } from '../../hooks/use_url_search_state'; +import type { SortField, SearchState } from '../../hooks/use_url_search_state'; import type { Option } from '../slo_context_menu'; import { ContextMenuItem, SLOContextMenu } from '../slo_context_menu'; -import type { SortField } from '../slo_list_search_bar'; export interface Props { onStateChange: (newState: Partial) => void; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx index 6e73cf875cf19..6ad7e054ed97e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx @@ -8,41 +8,46 @@ import { DefaultItemAction, EuiBasicTable, EuiBasicTableColumn, - EuiSkeletonRectangle, + EuiFlexGroup, + EuiIcon, EuiText, EuiToolTip, - EuiFlexGroup, } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; +import { rulesLocatorID, sloFeatureId } from '@kbn/observability-plugin/common'; +import { RulesParams } from '@kbn/observability-plugin/public'; +import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useQueryClient } from '@tanstack/react-query'; import React, { useState } from 'react'; -import { RulesParams } from '@kbn/observability-plugin/public'; -import { rulesLocatorID } from '@kbn/observability-plugin/common'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; -import { sloFeatureId } from '@kbn/observability-plugin/common'; -import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule_types'; -import { useKibana } from '../../../../utils/kibana_react'; -import { SloTagsList } from '../common/slo_tags_list'; -import { useCloneSlo } from '../../../../hooks/use_clone_slo'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { paths } from '../../../../../common/locators/paths'; import { SloDeleteConfirmationModal } from '../../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; import { SloStatusBadge } from '../../../../components/slo/slo_status_badge'; import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge'; import { sloKeys } from '../../../../hooks/query_key_factory'; import { useCapabilities } from '../../../../hooks/use_capabilities'; +import { useCloneSlo } from '../../../../hooks/use_clone_slo'; import { useDeleteSlo } from '../../../../hooks/use_delete_slo'; import { useFetchActiveAlerts } from '../../../../hooks/use_fetch_active_alerts'; import { useFetchHistoricalSummary } from '../../../../hooks/use_fetch_historical_summary'; import { useFetchRulesForSlo } from '../../../../hooks/use_fetch_rules_for_slo'; +import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule_types'; +import { useSpace } from '../../../../hooks/use_space'; +import { useKibana } from '../../../../utils/kibana_react'; import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter'; +import { + createRemoteSloDeleteUrl, + createRemoteSloEditUrl, +} from '../../../../utils/slo/remote_slo_urls'; +import { SloRemoteBadge } from '../badges/slo_remote_badge'; import { SloRulesBadge } from '../badges/slo_rules_badge'; +import { SLOGroupings } from '../common/slo_groupings'; +import { SloTagsList } from '../common/slo_tags_list'; import { SloListEmpty } from '../slo_list_empty'; import { SloListError } from '../slo_list_error'; import { SloSparkline } from '../slo_sparkline'; -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { SLOGroupings } from '../common/slo_groupings'; export interface Props { sloList: SLOWithSummaryResponse[]; @@ -60,6 +65,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { }, triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, } = useKibana().services; + const spaceId = useSpace(); const percentFormat = uiSettings.get('format:percent:defaultPattern'); const sloIdsAndInstanceIds = sloList.map( @@ -68,10 +74,8 @@ export function SloListCompactView({ sloList, loading, error }: Props) { const { hasWriteCapabilities } = useCapabilities(); const filteredRuleTypes = useGetFilteredRuleTypes(); - const queryClient = useQueryClient(); const { mutate: deleteSlo } = useDeleteSlo(); - const [sloToAddRule, setSloToAddRule] = useState(undefined); const [sloToDelete, setSloToDelete] = useState(undefined); @@ -96,11 +100,31 @@ export function SloListCompactView({ sloList, loading, error }: Props) { }); const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } = useFetchHistoricalSummary({ - list: sloList.map((slo) => ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })), + sloList, }); const navigateToClone = useCloneSlo(); + const isRemote = (slo: SLOWithSummaryResponse) => !!slo.remote; + const hasRemoteKibanaUrl = (slo: SLOWithSummaryResponse) => + !!slo.remote && slo.remote.kibanaUrl !== ''; + + const buildActionName = (actionName: string) => (slo: SLOWithSummaryResponse) => + isRemote(slo) ? ( + <> + {actionName} + + + ) : ( + actionName + ); + const actions: Array> = [ { type: 'icon', @@ -115,7 +139,10 @@ export function SloListCompactView({ sloList, loading, error }: Props) { const sloDetailsUrl = basePath.prepend( paths.sloDetails( slo.id, - ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined + ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId + ? slo.instanceId + : undefined, + slo.remote?.remoteName ) ); navigateToUrl(sloDetailsUrl); @@ -124,16 +151,23 @@ export function SloListCompactView({ sloList, loading, error }: Props) { { type: 'icon', icon: 'pencil', - name: i18n.translate('xpack.slo.item.actions.edit', { - defaultMessage: 'Edit', - }), + name: buildActionName( + i18n.translate('xpack.slo.item.actions.edit', { + defaultMessage: 'Edit', + }) + ), description: i18n.translate('xpack.slo.item.actions.edit', { defaultMessage: 'Edit', }), 'data-test-subj': 'sloActionsEdit', - enabled: (_) => hasWriteCapabilities, + enabled: (slo) => (hasWriteCapabilities && !isRemote(slo)) || hasRemoteKibanaUrl(slo), onClick: (slo: SLOWithSummaryResponse) => { - navigateToUrl(basePath.prepend(paths.sloEdit(slo.id))); + const remoteEditUrl = createRemoteSloEditUrl(slo, spaceId); + if (!!remoteEditUrl) { + window.open(remoteEditUrl, '_blank'); + } else { + navigateToUrl(basePath.prepend(paths.sloEdit(slo.id))); + } }, }, { @@ -146,7 +180,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { defaultMessage: 'Create new alert rule', }), 'data-test-subj': 'sloActionsCreateRule', - enabled: (_) => hasWriteCapabilities, + enabled: (slo: SLOWithSummaryResponse) => hasWriteCapabilities && !isRemote(slo), onClick: (slo: SLOWithSummaryResponse) => { setSloToAddRule(slo); }, @@ -161,7 +195,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { defaultMessage: 'Manage rules', }), 'data-test-subj': 'sloActionsManageRules', - enabled: (_) => hasWriteCapabilities, + enabled: (slo: SLOWithSummaryResponse) => hasWriteCapabilities && !isRemote(slo), onClick: (slo: SLOWithSummaryResponse) => { const locator = locators.get(rulesLocatorID); locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); @@ -170,14 +204,17 @@ export function SloListCompactView({ sloList, loading, error }: Props) { { type: 'icon', icon: 'copy', - name: i18n.translate('xpack.slo.item.actions.clone', { - defaultMessage: 'Clone', - }), + name: buildActionName( + i18n.translate('xpack.slo.item.actions.clone', { + defaultMessage: 'Clone', + }) + ), description: i18n.translate('xpack.slo.item.actions.clone', { defaultMessage: 'Clone', }), 'data-test-subj': 'sloActionsClone', - enabled: (_) => hasWriteCapabilities, + enabled: (slo: SLOWithSummaryResponse) => + (hasWriteCapabilities && !isRemote(slo)) || hasRemoteKibanaUrl(slo), onClick: (slo: SLOWithSummaryResponse) => { navigateToClone(slo); }, @@ -185,15 +222,25 @@ export function SloListCompactView({ sloList, loading, error }: Props) { { type: 'icon', icon: 'trash', - name: i18n.translate('xpack.slo.item.actions.delete', { - defaultMessage: 'Delete', - }), + name: buildActionName( + i18n.translate('xpack.slo.item.actions.delete', { + defaultMessage: 'Delete', + }) + ), description: i18n.translate('xpack.slo.item.actions.delete', { defaultMessage: 'Delete', }), 'data-test-subj': 'sloActionsDelete', - enabled: (_) => hasWriteCapabilities, - onClick: (slo: SLOWithSummaryResponse) => setSloToDelete(slo), + enabled: (slo: SLOWithSummaryResponse) => + (hasWriteCapabilities && !isRemote(slo)) || hasRemoteKibanaUrl(slo), + onClick: (slo: SLOWithSummaryResponse) => { + const remoteDeleteUrl = createRemoteSloDeleteUrl(slo, spaceId); + if (!!remoteDeleteUrl) { + window.open(remoteDeleteUrl, '_blank'); + } else { + setSloToDelete(slo); + } + }, }, ]; @@ -201,20 +248,12 @@ export function SloListCompactView({ sloList, loading, error }: Props) { { field: 'status', name: 'Status', - render: (_, slo: SLOWithSummaryResponse) => - !slo.summary ? ( - - ) : ( - - - - ), + render: (_, slo: SLOWithSummaryResponse) => ( + + + + + ), }, { field: 'alerts', @@ -223,7 +262,11 @@ export function SloListCompactView({ sloList, loading, error }: Props) { width: '5%', render: (_, slo: SLOWithSummaryResponse) => ( <> - setSloToAddRule(slo)} /> + setSloToAddRule(slo)} + isRemote={!!slo.remote} + /> - {slo.summary ? ( - - {slo.name} - - ) : ( - {slo.name} - )} + + {slo.name} + ); @@ -287,7 +329,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { name: 'SLI value', truncateText: true, render: (_, slo: SLOWithSummaryResponse) => - !slo.summary || slo.summary.status === 'NO_DATA' + slo.summary.status === 'NO_DATA' ? NOT_AVAILABLE_LABEL : numeral(slo.summary.sliValue).format(percentFormat), }, @@ -295,9 +337,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { field: 'historicalSli', name: 'Historical SLI', render: (_, slo: SLOWithSummaryResponse) => { - const isSloFailed = - (slo.summary && slo.summary.status === 'VIOLATED') || - (slo.summary && slo.summary.status === 'DEGRADING'); + const isSloFailed = ['VIOLATED', 'DEGRADING'].includes(slo.summary.status); const historicalSliData = formatHistoricalData( historicalSummaries.find( (historicalSummary) => @@ -323,7 +363,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { name: 'Budget remaining', truncateText: true, render: (_, slo: SLOWithSummaryResponse) => - !slo.summary || slo.summary.status === 'NO_DATA' + slo.summary.status === 'NO_DATA' ? NOT_AVAILABLE_LABEL : numeral(slo.summary.errorBudget.remaining).format(percentFormat), }, @@ -331,9 +371,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { field: 'historicalErrorBudgetRemaining', name: 'Historical budget remaining', render: (_, slo: SLOWithSummaryResponse) => { - const isSloFailed = - (slo.summary && slo.summary.status === 'VIOLATED') || - (slo.summary && slo.summary.status === 'DEGRADING'); + const isSloFailed = ['VIOLATED', 'DEGRADING'].includes(slo.summary.status); const errorBudgetBurnDownData = formatHistoricalData( historicalSummaries.find( (historicalSummary) => diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx index 30e24098e918b..ba5f53f531e6a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx @@ -30,8 +30,8 @@ import { useFetchSloList } from '../../../../hooks/use_fetch_slo_list'; import { SLI_OPTIONS } from '../../../slo_edit/constants'; import { useSloFormattedSLIValue } from '../../hooks/use_slo_summary'; import { SlosView } from '../slos_view'; -import type { SortDirection } from '../slo_list_search_bar'; import { SLOView } from '../toggle_slo_view'; +import type { SortDirection } from '../../hooks/use_url_search_state'; interface Props { group: string; @@ -54,7 +54,8 @@ export function GroupListView({ summary, filters, }: Props) { - const query = kqlQuery ? `"${groupBy}": (${group}) and ${kqlQuery}` : `"${groupBy}": ${group}`; + const groupQuery = `"${groupBy}": "${group}"`; + const query = kqlQuery ? `"${groupQuery}) and ${kqlQuery}` : groupQuery; let groupName = group.toLowerCase(); if (groupBy === 'slo.indicator.type') { groupName = SLI_OPTIONS.find((option) => option.value === group)?.text ?? group; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.tsx index 8700af0a127e6..f268ecd0028ea 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.tsx @@ -9,11 +9,11 @@ import React, { useEffect } from 'react'; import { Filter } from '@kbn/es-query'; import { useFetchSloGroups } from '../../../../hooks/use_fetch_slo_groups'; import { useUrlSearchState } from '../../hooks/use_url_search_state'; -import type { SortDirection } from '../slo_list_search_bar'; import { SLOView } from '../toggle_slo_view'; import { SloGroupListEmpty } from './group_list_empty'; import { SloGroupListError } from './group_list_error'; import { GroupListView } from './group_list_view'; +import type { SortDirection } from '../../hooks/use_url_search_state'; interface Props { groupBy: string; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx index 3d6f60051c075..2f6ce7da224e8 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx @@ -6,25 +6,24 @@ */ import { EuiButtonIcon, + EuiButtonIconProps, EuiContextMenuItem, EuiContextMenuPanel, + EuiIcon, + EuiPanel, EuiPopover, - EuiButtonIconProps, useEuiShadow, - EuiPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import React from 'react'; -import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import styled from 'styled-components'; -import { RulesParams } from '@kbn/observability-plugin/public'; -import { rulesLocatorID } from '@kbn/observability-plugin/common'; -import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { useCapabilities } from '../../../hooks/use_capabilities'; +import { useCloneSlo } from '../../../hooks/use_clone_slo'; import { BurnRateRuleParams } from '../../../typings'; import { useKibana } from '../../../utils/kibana_react'; -import { useCloneSlo } from '../../../hooks/use_clone_slo'; -import { useCapabilities } from '../../../hooks/use_capabilities'; -import { paths } from '../../../../common/locators/paths'; +import { useSloActions } from '../../slo_details/hooks/use_slo_actions'; interface Props { slo: SLOWithSummaryResponse; @@ -71,22 +70,19 @@ export function SloItemActions({ }: Props) { const { application: { navigateToUrl }, - http: { basePath }, - share: { - url: { locators }, - }, executionContext, } = useKibana().services; const executionContextName = executionContext.get().name; const isDashboardContext = executionContextName === 'dashboards'; const { hasWriteCapabilities } = useCapabilities(); + const navigateToClone = useCloneSlo(); - const sloDetailsUrl = basePath.prepend( - paths.sloDetails( - slo.id, - ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined - ) - ); + const { handleNavigateToRules, sloEditUrl, remoteDeleteUrl, sloDetailsUrl } = useSloActions({ + slo, + rules, + setIsEditRuleFlyoutOpen, + setIsActionsPopoverOpen, + }); const handleClickActions = () => { setIsActionsPopoverOpen(!isActionsPopoverOpen); @@ -96,32 +92,19 @@ export function SloItemActions({ navigateToUrl(sloDetailsUrl); }; - const handleEdit = () => { - navigateToUrl(basePath.prepend(paths.sloEdit(slo.id))); - }; - - const navigateToClone = useCloneSlo(); - const handleClone = () => { navigateToClone(slo); }; - const handleNavigateToRules = async () => { - if (rules?.length === 1) { - // if there is only one rule we can edit inline in flyout - setIsEditRuleFlyoutOpen(true); - setIsActionsPopoverOpen(false); + const handleDelete = () => { + if (!!remoteDeleteUrl) { + window.open(remoteDeleteUrl, '_blank'); } else { - const locator = locators.get(rulesLocatorID); - locator?.navigate({ params: { sloId: slo.id } }, { replace: false }); + setDeleteConfirmationModalOpen(true); + setIsActionsPopoverOpen(false); } }; - const handleDelete = () => { - setDeleteConfirmationModalOpen(true); - setIsActionsPopoverOpen(false); - }; - const handleCreateRule = () => { setIsActionsPopoverOpen(false); setIsAddRuleFlyoutOpen(true); @@ -150,6 +133,19 @@ export function SloItemActions({ /> ); + const isRemote = !!slo.remote; + const hasUndefinedRemoteKibanaUrl = !!slo.remote && slo.remote.kibanaUrl === ''; + + const showRemoteLinkIcon = isRemote ? ( + + ) : null; + return ( {i18n.translate('xpack.slo.item.actions.edit', { defaultMessage: 'Edit', })} + {showRemoteLinkIcon} , {i18n.translate('xpack.slo.item.actions.createRule', { defaultMessage: 'Create new alert rule', @@ -196,32 +198,44 @@ export function SloItemActions({ {i18n.translate('xpack.slo.item.actions.manageBurnRateRules', { defaultMessage: 'Manage burn rate {count, plural, one {rule} other {rules}}', values: { count: rules?.length ?? 0 }, })} + {showRemoteLinkIcon} , {i18n.translate('xpack.slo.item.actions.clone', { defaultMessage: 'Clone' })} + {showRemoteLinkIcon} , {i18n.translate('xpack.slo.item.actions.delete', { defaultMessage: 'Delete' })} + {showRemoteLinkIcon} , ].concat( !isDashboardContext ? ( @@ -243,3 +257,14 @@ export function SloItemActions({ ); } + +const NOT_AVAILABLE_FOR_REMOTE = i18n.translate('xpack.slo.item.actions.notAvailable', { + defaultMessage: 'This action is not available for remote SLOs', +}); + +const NOT_AVAILABLE_FOR_UNDEFINED_REMOTE_KIBANA_URL = i18n.translate( + 'xpack.slo.item.actions.remoteKibanaUrlUndefined', + { + defaultMessage: 'This action is not available for remote SLOs with undefined kibanaUrl', + } +); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx index 75aaa869d5d88..b2764a4b9ab19 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx @@ -8,11 +8,12 @@ import { EuiPanel, EuiSelectableOption, EuiText } from '@elastic/eui'; import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; +import { useGetSettings } from '../../slo_settings/use_get_settings'; import type { SearchState } from '../hooks/use_url_search_state'; import type { Option } from './slo_context_menu'; import { ContextMenuItem, SLOContextMenu } from './slo_context_menu'; -export type GroupByField = 'ungrouped' | 'slo.tags' | 'status' | 'slo.indicator.type'; +export type GroupByField = 'ungrouped' | 'slo.tags' | 'status' | 'slo.indicator.type' | '_index'; export interface Props { onStateChange: (newState: Partial) => void; state: SearchState; @@ -29,6 +30,11 @@ export function SloGroupBy({ onStateChange, state, loading }: Props) { const [isGroupByPopoverOpen, setIsGroupByPopoverOpen] = useState(false); const groupBy = state.groupBy; + const { data: settings } = useGetSettings(); + + const hasRemoteEnabled = + settings && (settings.useAllRemoteClusters || settings.selectedRemoteClusters.length > 0); + const handleChangeGroupBy = (value: GroupByField) => { onStateChange({ page: 0, @@ -76,6 +82,19 @@ export function SloGroupBy({ onStateChange, state, loading }: Props) { }, ]; + if (hasRemoteEnabled) { + groupByOptions.push({ + label: i18n.translate('xpack.slo.list.groupBy.remoteCluster', { + defaultMessage: 'Remote cluster', + }), + checked: groupBy === '_index', + value: '_index', + onClick: () => { + handleChangeGroupBy('_index'); + }, + }); + } + const items = [ diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx index 8b42d6f02506b..69cc895121d01 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx @@ -5,31 +5,17 @@ * 2.0. */ -import { EuiSelectableOption } from '@elastic/eui'; -import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; import { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { observabilityAppId } from '@kbn/observability-plugin/public'; import React, { useEffect } from 'react'; import styled from 'styled-components'; -import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../../common/constants'; -import { useCreateDataView } from '../../../hooks/use_create_data_view'; import { useKibana } from '../../../utils/kibana_react'; import { useSloCrudLoading } from '../hooks/use_crud_loading'; +import { useSloSummaryDataView } from '../hooks/use_summary_dataview'; import { useUrlSearchState } from '../hooks/use_url_search_state'; import { QuickFilters } from './common/quick_filters'; -export type SortField = 'sli_value' | 'error_budget_consumed' | 'error_budget_remaining' | 'status'; -export type SortDirection = 'asc' | 'desc'; - -export type Item = EuiSelectableOption & { - label: string; - type: T; - checked?: EuiSelectableOptionCheckedType; -}; - -export type ViewMode = 'default' | 'compact'; - export function SloListSearchBar() { const { data: { query }, @@ -39,11 +25,9 @@ export function SloListSearchBar() { } = useKibana().services; const { state, onStateChange } = useUrlSearchState(); - const loading = useSloCrudLoading(); + const isSloCrudLoading = useSloCrudLoading(); - const { dataView } = useCreateDataView({ - indexPatternString: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - }); + const { isLoading: isDataViewLoading, data: dataView } = useSloSummaryDataView(); useEffect(() => { const sub = query.state$.subscribe(() => { @@ -63,9 +47,14 @@ export function SloListSearchBar() { appName={observabilityAppId} placeholder={PLACEHOLDER} indexPatterns={dataView ? [dataView] : []} - isDisabled={loading} + isDisabled={isSloCrudLoading} renderQueryInputAppend={() => ( - + )} filters={state.filters} onFiltersUpdated={(newFilters) => { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_item.stories.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_item.stories.tsx similarity index 90% rename from x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_item.stories.tsx rename to x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_item.stories.tsx index 87e9f75a0c65e..ae6f75186917c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_item.stories.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_item.stories.tsx @@ -12,8 +12,8 @@ import { KibanaReactStorybookDecorator } from '@kbn/observability-plugin/public' import { HEALTHY_ROLLING_SLO, historicalSummaryData, -} from '../../../data/slo/historical_summary_data'; -import { buildSlo } from '../../../data/slo/slo'; +} from '../../../../data/slo/historical_summary_data'; +import { buildSlo } from '../../../../data/slo/slo'; import { SloListItem as Component, SloListItemProps } from './slo_list_item'; export default { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_item.tsx similarity index 72% rename from x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_item.tsx rename to x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_item.tsx index 44a672e82b7dd..ca81607dab863 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_item.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_item.tsx @@ -9,16 +9,16 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import React, { useState } from 'react'; -import { EditBurnRateRuleFlyout } from './common/edit_burn_rate_rule_flyout'; -import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; -import { useSloFormattedSummary } from '../hooks/use_slo_summary'; -import { BurnRateRuleFlyout } from './common/burn_rate_rule_flyout'; -import { useSloListActions } from '../hooks/use_slo_list_actions'; -import { SloItemActions } from './slo_item_actions'; -import { SloBadges } from './badges/slo_badges'; -import { SloSummary } from './slo_summary'; -import { BurnRateRuleParams } from '../../../typings'; -import { SLOGroupings } from './common/slo_groupings'; +import { EditBurnRateRuleFlyout } from '../common/edit_burn_rate_rule_flyout'; +import { SloDeleteConfirmationModal } from '../../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; +import { useSloFormattedSummary } from '../../hooks/use_slo_summary'; +import { BurnRateRuleFlyout } from '../common/burn_rate_rule_flyout'; +import { useSloListActions } from '../../hooks/use_slo_list_actions'; +import { SloItemActions } from '../slo_item_actions'; +import { SloBadges } from '../badges/slo_badges'; +import { SloSummary } from '../slo_summary'; +import { BurnRateRuleParams } from '../../../../typings'; +import { SLOGroupings } from '../common/slo_groupings'; export interface SloListItemProps { slo: SLOWithSummaryResponse; @@ -61,16 +61,12 @@ export function SloListItem({ - {slo.summary ? ( - <> - - {slo.name} - - - - ) : ( - {slo.name} - )} + <> + + {slo.name} + + + - {slo.summary ? ( - - ) : null} + diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_view.tsx index 9816497f3f545..a97c99d470336 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_view/slo_list_view.tsx @@ -13,7 +13,7 @@ import { useFetchHistoricalSummary } from '../../../../hooks/use_fetch_historica import { useFetchRulesForSlo } from '../../../../hooks/use_fetch_rules_for_slo'; import { SloListEmpty } from '../slo_list_empty'; import { SloListError } from '../slo_list_error'; -import { SloListItem } from '../slo_list_item'; +import { SloListItem } from './slo_list_item'; export interface Props { sloList: SLOWithSummaryResponse[]; @@ -31,7 +31,7 @@ export function SloListView({ sloList, loading, error }: Props) { }); const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } = useFetchHistoricalSummary({ - list: sloList.map((slo) => ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })), + sloList, }); if (!loading && !error && sloList.length === 0) { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_view.tsx index ca093ccef1043..e317d33290cb2 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_view.tsx @@ -26,20 +26,30 @@ export function SlosView({ sloList, loading, error, sloView }: Props) { if (!loading && !error && sloList.length === 0) { return ; } + if (!loading && error) { return ; } - return sloView === 'cardView' ? ( - - - - ) : ( - - {sloView === 'compactView' && ( + if (sloView === 'cardView') { + return ( + + + + ); + } + + if (sloView === 'compactView') { + return ( + - )} - {sloView === 'listView' && } + + ); + } + + return ( + + ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts index 2fbb4009dbe5e..f13cca6658ce0 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts @@ -17,13 +17,11 @@ export function useSloListActions({ setIsAddRuleFlyoutOpen, setIsActionsPopoverOpen, setDeleteConfirmationModalOpen, - setDashboardAttachmentReady, }: { slo: SLOWithSummaryResponse; setIsActionsPopoverOpen: (val: boolean) => void; setIsAddRuleFlyoutOpen: (val: boolean) => void; setDeleteConfirmationModalOpen: (val: boolean) => void; - setDashboardAttachmentReady?: (val: boolean) => void; }) { const { embeddable } = useKibana().services; const { mutate: deleteSlo } = useDeleteSlo(); @@ -49,6 +47,7 @@ export function useSloListActions({ description: newDescription, sloId: slo.id, sloInstanceId: slo.instanceId, + remoteName: slo.remote?.remoteName, }; const state = { @@ -63,7 +62,7 @@ export function useSloListActions({ path, }); }, - [embeddable, slo.id, slo.instanceId] + [embeddable, slo.id, slo.instanceId, slo.remote?.remoteName] ); return { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts index 8f9e14596f1bb..d2811774e59aa 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts @@ -48,7 +48,8 @@ export const getSloFormattedSummary = ( const sloDetailsUrl = basePath.prepend( paths.sloDetails( slo.id, - ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined + ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined, + slo.remote?.remoteName ) ); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts new file mode 100644 index 0000000000000..3d502cfe85ec7 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts @@ -0,0 +1,39 @@ +/* + * 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 { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { useEffect, useState } from 'react'; +import { getListOfSloSummaryIndices } from '../../../../common/summary_indices'; +import { useCreateDataView } from '../../../hooks/use_create_data_view'; +import { useKibana } from '../../../utils/kibana_react'; +import { useGetSettings } from '../../slo_settings/use_get_settings'; + +export const useSloSummaryDataView = () => { + const { http } = useKibana().services; + + const [indexPattern, setIndexPattern] = useState(); + const { isLoading: isSettingsLoading, data: settings } = useGetSettings(); + const { loading: isRemoteClustersLoading, data: remoteClusters } = useFetcher(() => { + return http?.get>('/api/remote_clusters'); + }, [http]); + + useEffect(() => { + if (settings && remoteClusters) { + const summaryIndices = getListOfSloSummaryIndices(settings, remoteClusters); + setIndexPattern(summaryIndices); + } + }, [settings, remoteClusters]); + + const { loading: isDataViewLoading, dataView } = useCreateDataView({ + indexPatternString: indexPattern, + }); + + return { + isLoading: isSettingsLoading || isRemoteClustersLoading || isDataViewLoading, + data: dataView, + }; +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts index dfbfa1b5ddeb0..a4b842c2ee19f 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts @@ -11,11 +11,12 @@ import { useHistory } from 'react-router-dom'; import { Filter } from '@kbn/es-query'; import { useCallback, useEffect, useRef, useState } from 'react'; import { DEFAULT_SLO_PAGE_SIZE } from '../../../../common/constants'; -import type { SortField, SortDirection } from '../components/slo_list_search_bar'; import type { GroupByField } from '../components/slo_list_group_by'; import type { SLOView } from '../components/toggle_slo_view'; export const SLO_LIST_SEARCH_URL_STORAGE_KEY = 'search'; +export type SortField = 'sli_value' | 'error_budget_consumed' | 'error_budget_remaining' | 'status'; +export type SortDirection = 'asc' | 'desc'; export interface SearchState { kqlQuery: string; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx index 3a9300b581f3f..7707c0c41ad08 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx @@ -25,6 +25,7 @@ import { HeaderMenuPortal, TagsList } from '@kbn/observability-shared-plugin/pub import { useKibana } from '../../utils/kibana_react'; import { render } from '../../utils/test_helper'; import { SlosPage } from './slos'; +import { useGetSettings } from '../slo_settings/use_get_settings'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -36,10 +37,12 @@ jest.mock('../../utils/kibana_react'); jest.mock('../../hooks/use_license'); jest.mock('../../hooks/use_fetch_slo_list'); jest.mock('../../hooks/use_create_slo'); +jest.mock('../slo_settings/use_get_settings'); jest.mock('../../hooks/use_delete_slo'); jest.mock('../../hooks/use_fetch_historical_summary'); jest.mock('../../hooks/use_capabilities'); +const useGetSettingsMock = useGetSettings as jest.Mock; const useKibanaMock = useKibana as jest.Mock; const useLicenseMock = useLicense as jest.Mock; const useFetchSloListMock = useFetchSloList as jest.Mock; @@ -137,6 +140,13 @@ describe('SLOs Page', () => { beforeEach(() => { jest.clearAllMocks(); mockKibana(); + useGetSettingsMock.mockReturnValue({ + isLoading: false, + data: { + useAllRemoteClusters: false, + selectedRemoteClusters: [], + }, + }); useCapabilitiesMock.mockReturnValue({ hasWriteCapabilities: true, hasReadCapabilities: true }); jest .spyOn(Router, 'useLocation') diff --git a/x-pack/plugins/observability_solution/slo/public/routes/routes.tsx b/x-pack/plugins/observability_solution/slo/public/routes/routes.tsx index 7ba21c8befa0f..98243caec301e 100644 --- a/x-pack/plugins/observability_solution/slo/public/routes/routes.tsx +++ b/x-pack/plugins/observability_solution/slo/public/routes/routes.tsx @@ -17,8 +17,10 @@ import { SLO_CREATE_PATH, SLO_DETAIL_PATH, SLO_EDIT_PATH, + SLO_SETTINGS_PATH, } from '../../common/locators/paths'; import { SlosOutdatedDefinitions } from '../pages/slo_outdated_definitions'; +import { SloSettingsPage } from '../pages/slo_settings/slo_settings'; export const routes = { [SLOS_PATH]: { @@ -56,6 +58,13 @@ export const routes = { params: {}, exact: true, }, + [SLO_SETTINGS_PATH]: { + handler: () => { + return ; + }, + params: {}, + exact: true, + }, [SLO_DETAIL_PATH]: { handler: () => { return ; diff --git a/x-pack/plugins/observability_solution/slo/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.ts b/x-pack/plugins/observability_solution/slo/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.ts index 89f60db107b14..f12bc9492dee7 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.ts +++ b/x-pack/plugins/observability_solution/slo/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.ts @@ -10,10 +10,13 @@ import { apmTransactionDurationIndicatorSchema, apmTransactionErrorRateIndicatorSchema, kqlQuerySchema, - SLOResponse, + SLODefinitionResponse, + SLOWithSummaryResponse, } from '@kbn/slo-schema'; -export function convertSliApmParamsToApmAppDeeplinkUrl(slo: SLOResponse): string | undefined { +export function convertSliApmParamsToApmAppDeeplinkUrl( + slo: SLOWithSummaryResponse | SLODefinitionResponse +): string | undefined { if ( !apmTransactionDurationIndicatorSchema.is(slo.indicator) && !apmTransactionErrorRateIndicatorSchema.is(slo.indicator) @@ -27,7 +30,6 @@ export function convertSliApmParamsToApmAppDeeplinkUrl(slo: SLOResponse): string }, timeWindow: { duration }, groupBy, - instanceId, } = slo; const qs = new URLSearchParams('comparisonEnabled=true'); @@ -52,8 +54,9 @@ export function convertSliApmParamsToApmAppDeeplinkUrl(slo: SLOResponse): string if (filter && kqlQuerySchema.is(filter) && filter.length > 0) { kueryParams.push(filter); } - if (groupBy !== ALL_VALUE && instanceId !== ALL_VALUE) { - kueryParams.push(`${groupBy} : "${instanceId}"`); + + if (groupBy !== ALL_VALUE && 'instanceId' in slo && slo.instanceId !== ALL_VALUE) { + kueryParams.push(`${groupBy} : "${slo.instanceId}"`); } if (kueryParams.length > 0) { diff --git a/x-pack/plugins/observability_solution/slo/public/utils/slo/get_delay_in_seconds_from_slo.ts b/x-pack/plugins/observability_solution/slo/public/utils/slo/get_delay_in_seconds_from_slo.ts index 2901f8a0067e7..923ee7515faca 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/slo/get_delay_in_seconds_from_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/utils/slo/get_delay_in_seconds_from_slo.ts @@ -5,10 +5,14 @@ * 2.0. */ -import { SLOResponse, timeslicesBudgetingMethodSchema, durationType } from '@kbn/slo-schema'; +import { + SLODefinitionResponse, + timeslicesBudgetingMethodSchema, + durationType, +} from '@kbn/slo-schema'; import { isLeft } from 'fp-ts/lib/Either'; -export function getDelayInSecondsFromSLO(slo: SLOResponse) { +export function getDelayInSecondsFromSLO(slo: SLODefinitionResponse) { const fixedInterval = timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) ? durationStringToSeconds(slo.objective.timesliceWindow) : 60; diff --git a/x-pack/plugins/observability_solution/slo/public/utils/slo/get_discover_link.ts b/x-pack/plugins/observability_solution/slo/public/utils/slo/get_discover_link.ts index ab09353becb92..6166a56ae73ba 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/slo/get_discover_link.ts +++ b/x-pack/plugins/observability_solution/slo/public/utils/slo/get_discover_link.ts @@ -136,7 +136,9 @@ function createDiscoverLocator( filters, dataViewSpec: { id: indexId, - title: slo.indicator.params.index, + title: slo.remote + ? `${slo.remote.remoteName}:${slo.indicator.params.index}` + : slo.indicator.params.index, timeFieldName, }, }; diff --git a/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.test.ts b/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.test.ts new file mode 100644 index 0000000000000..28ca2bd977572 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { buildSlo } from '../../data/slo/slo'; +import { + createRemoteSloCloneUrl, + createRemoteSloDeleteUrl, + createRemoteSloDetailsUrl, + createRemoteSloEditUrl, +} from './remote_slo_urls'; + +describe('remote SLO URLs Utils', () => { + it('returns undefined for local SLOs', () => { + const localSlo = buildSlo({ id: 'fixed-id' }); + + expect(createRemoteSloDetailsUrl(localSlo)).toMatchInlineSnapshot(`undefined`); + expect(createRemoteSloDeleteUrl(localSlo)).toMatchInlineSnapshot(`undefined`); + expect(createRemoteSloEditUrl(localSlo)).toMatchInlineSnapshot(`undefined`); + expect(createRemoteSloCloneUrl(localSlo)).toMatchInlineSnapshot(`undefined`); + }); + + it('returns undefined for remote SLOs with empty kibanaUrl', () => { + const remoteSloWithoutKibanaUrl = buildSlo({ + id: 'fixed-id', + remote: { kibanaUrl: '', remoteName: 'remote_cluster' }, + }); + + expect(createRemoteSloDetailsUrl(remoteSloWithoutKibanaUrl)).toMatchInlineSnapshot(`undefined`); + expect(createRemoteSloDeleteUrl(remoteSloWithoutKibanaUrl)).toMatchInlineSnapshot(`undefined`); + expect(createRemoteSloEditUrl(remoteSloWithoutKibanaUrl)).toMatchInlineSnapshot(`undefined`); + expect(createRemoteSloCloneUrl(remoteSloWithoutKibanaUrl)).toMatchInlineSnapshot(`undefined`); + }); + + it('returns the correct URLs for remote SLOs with kibanaUrl', () => { + const remoteSlo = buildSlo({ + id: 'fixed-id', + remote: { kibanaUrl: 'https://cloud.elast.co/kibana', remoteName: 'remote_cluster' }, + }); + + expect(createRemoteSloDetailsUrl(remoteSlo)).toMatchInlineSnapshot( + `"https://cloud.elast.co/app/slos/fixed-id?"` + ); + expect(createRemoteSloDeleteUrl(remoteSlo)).toMatchInlineSnapshot( + `"https://cloud.elast.co/app/slos/fixed-id?delete=true"` + ); + expect(createRemoteSloEditUrl(remoteSlo)).toMatchInlineSnapshot( + `"https://cloud.elast.co/app/slos/edit/fixed-id"` + ); + expect(createRemoteSloCloneUrl(remoteSlo)).toMatchInlineSnapshot( + `"https://cloud.elast.co/app/slos/create?_a=(budgetingMethod:occurrences,createdAt:%272022-12-29T10:11:12.000Z%27,description:%27some%20description%20useful%27,enabled:!t,groupBy:%27*%27,groupings:(),indicator:(params:(filter:%27baz:%20foo%20and%20bar%20%3E%202%27,good:%27http_status:%202xx%27,index:some-index,timestampField:custom_timestamp,total:%27a%20query%27),type:sli.kql.custom),instanceId:%27*%27,meta:(),name:%27[Copy]%20super%20important%20level%20service%27,objective:(target:0.98),remote:(kibanaUrl:%27https:/cloud.elast.co/kibana%27,remoteName:remote_cluster),revision:1,settings:(frequency:%271m%27,syncDelay:%271m%27),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:%2730d%27,type:rolling),updatedAt:%272022-12-29T10:11:12.000Z%27,version:2)"` + ); + }); + + it('returns the correct URLs including spaceId for remote SLOs with kibanaUrl', () => { + const remoteSlo = buildSlo({ + id: 'fixed-id', + remote: { kibanaUrl: 'https://cloud.elast.co/kibana', remoteName: 'remote_cluster' }, + }); + + expect(createRemoteSloDetailsUrl(remoteSlo, 'my-custom-space')).toMatchInlineSnapshot( + `"https://cloud.elast.co/s/my-custom-space/app/slos/fixed-id?"` + ); + expect(createRemoteSloDeleteUrl(remoteSlo, 'my-custom-space')).toMatchInlineSnapshot( + `"https://cloud.elast.co/s/my-custom-space/app/slos/fixed-id?delete=true"` + ); + expect(createRemoteSloEditUrl(remoteSlo, 'my-custom-space')).toMatchInlineSnapshot( + `"https://cloud.elast.co/s/my-custom-space/app/slos/edit/fixed-id"` + ); + expect(createRemoteSloCloneUrl(remoteSlo, 'my-custom-space')).toMatchInlineSnapshot( + `"https://cloud.elast.co/s/my-custom-space/app/slos/create?_a=(budgetingMethod:occurrences,createdAt:%272022-12-29T10:11:12.000Z%27,description:%27some%20description%20useful%27,enabled:!t,groupBy:%27*%27,groupings:(),indicator:(params:(filter:%27baz:%20foo%20and%20bar%20%3E%202%27,good:%27http_status:%202xx%27,index:some-index,timestampField:custom_timestamp,total:%27a%20query%27),type:sli.kql.custom),instanceId:%27*%27,meta:(),name:%27[Copy]%20super%20important%20level%20service%27,objective:(target:0.98),remote:(kibanaUrl:%27https:/cloud.elast.co/kibana%27,remoteName:remote_cluster),revision:1,settings:(frequency:%271m%27,syncDelay:%271m%27),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:%2730d%27,type:rolling),updatedAt:%272022-12-29T10:11:12.000Z%27,version:2)"` + ); + }); +}); diff --git a/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.ts b/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.ts new file mode 100644 index 0000000000000..2e6c8bed91ba4 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.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 { encode } from '@kbn/rison'; +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import path from 'path'; +import { paths } from '../../../common/locators/paths'; + +export function createRemoteSloDetailsUrl( + slo: SLOWithSummaryResponse, + spaceId: string = 'default' +) { + if (!slo.remote || slo.remote.kibanaUrl === '') { + return undefined; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const detailsPath = paths.sloDetails( + slo.id, + ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined + ); + + const remoteUrl = new URL(path.join(spacePath, detailsPath), slo.remote.kibanaUrl); + return remoteUrl.toString(); +} + +export function createRemoteSloDeleteUrl(slo: SLOWithSummaryResponse, spaceId: string = 'default') { + if (!slo.remote || slo.remote.kibanaUrl === '') { + return undefined; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const detailsPath = paths.sloDetails( + slo.id, + ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined + ); + + const remoteUrl = new URL(path.join(spacePath, detailsPath), slo.remote.kibanaUrl); + remoteUrl.searchParams.append('delete', 'true'); + + return remoteUrl.toString(); +} + +export function createRemoteSloEditUrl(slo: SLOWithSummaryResponse, spaceId: string = 'default') { + if (!slo.remote || slo.remote.kibanaUrl === '') { + return undefined; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const editPath = paths.sloEdit(slo.id); + const remoteUrl = new URL(path.join(spacePath, editPath), slo.remote.kibanaUrl); + + return remoteUrl.toString(); +} + +export function createRemoteSloCloneUrl(slo: SLOWithSummaryResponse, spaceId: string = 'default') { + if (!slo.remote || slo.remote.kibanaUrl === '') { + return undefined; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const clonePath = paths.sloCreateWithEncodedForm( + encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined }) + ); + const remoteUrl = new URL(path.join(spacePath, clonePath), slo.remote.kibanaUrl); + return remoteUrl.toString(); +} diff --git a/x-pack/plugins/observability_solution/slo/server/assets/ingest_templates/slo_summary_pipeline_template.ts b/x-pack/plugins/observability_solution/slo/server/assets/ingest_templates/slo_summary_pipeline_template.ts index 967a1f4af6e91..d3b4aed69f1c0 100644 --- a/x-pack/plugins/observability_solution/slo/server/assets/ingest_templates/slo_summary_pipeline_template.ts +++ b/x-pack/plugins/observability_solution/slo/server/assets/ingest_templates/slo_summary_pipeline_template.ts @@ -9,10 +9,10 @@ import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; import { IBasePath } from '@kbn/core-http-server'; import { getSLOSummaryPipelineId, SLO_RESOURCES_VERSION } from '../../../common/constants'; -import { SLO } from '../../domain/models'; +import { SLODefinition } from '../../domain/models'; export const getSLOSummaryPipelineTemplate = ( - slo: SLO, + slo: SLODefinition, spaceId: string, basePath: IBasePath ): IngestPutPipelineRequest => { @@ -166,8 +166,10 @@ export const getSLOSummaryPipelineTemplate = ( value: spaceId, }, }, + // >= 8.14: { set: { + description: 'Store the indicator params', field: 'slo.indicator.params', value: slo.indicator.params, ignore_failure: true, diff --git a/x-pack/plugins/observability_solution/slo/server/assets/transform_templates/slo_transform_template.ts b/x-pack/plugins/observability_solution/slo/server/assets/transform_templates/slo_transform_template.ts index 23b57d364606c..4381ebff49fad 100644 --- a/x-pack/plugins/observability_solution/slo/server/assets/transform_templates/slo_transform_template.ts +++ b/x-pack/plugins/observability_solution/slo/server/assets/transform_templates/slo_transform_template.ts @@ -15,7 +15,7 @@ import { } from '@elastic/elasticsearch/lib/api/types'; import { ALL_VALUE } from '@kbn/slo-schema'; import { SLO_RESOURCES_VERSION } from '../../../common/constants'; -import { SLO } from '../../domain/models'; +import { SLODefinition } from '../../domain/models'; export interface TransformSettings { frequency: TransformPutTransformRequest['frequency']; @@ -31,7 +31,7 @@ export const getSLOTransformTemplate = ( groupBy: TransformPivot['group_by'] = {}, aggregations: TransformPivot['aggregations'] = {}, settings: TransformSettings, - slo: SLO + slo: SLODefinition ): TransformPutTransformRequest => { const formattedSource = buildSourceWithFilters(source, slo); return { @@ -63,13 +63,13 @@ export const getSLOTransformTemplate = ( }; }; -const buildGroupingFilters = (slo: SLO): QueryDslQueryContainer[] => { +const buildGroupingFilters = (slo: SLODefinition): QueryDslQueryContainer[] => { // build exists filters for each groupBy field to make sure the field exists const groups = [slo.groupBy].flat().filter((group) => !!group && group !== ALL_VALUE); return groups.map((group) => ({ exists: { field: group } })); }; -const buildSourceWithFilters = (source: TransformSource, slo: SLO): TransformSource => { +const buildSourceWithFilters = (source: TransformSource, slo: SLODefinition): TransformSource => { const groupingFilters = buildGroupingFilters(slo); const sourceFilters = [source.query?.bool?.filter].flat() || []; return { diff --git a/x-pack/plugins/observability_solution/slo/server/domain/models/common.ts b/x-pack/plugins/observability_solution/slo/server/domain/models/common.ts index e1f747cde8d88..06eb5e11fd2ba 100644 --- a/x-pack/plugins/observability_solution/slo/server/domain/models/common.ts +++ b/x-pack/plugins/observability_solution/slo/server/domain/models/common.ts @@ -8,14 +8,18 @@ import * as t from 'io-ts'; import { dateRangeSchema, - historicalSummarySchema, - statusSchema, - summarySchema, + groupBySchema, groupingsSchema, groupSummarySchema, + historicalSummarySchema, metaSchema, + objectiveSchema, + sloSettingsSchema, + statusSchema, + summarySchema, } from '@kbn/slo-schema'; +type Objective = t.TypeOf; type Status = t.TypeOf; type DateRange = t.TypeOf; type HistoricalSummary = t.TypeOf; @@ -23,5 +27,18 @@ type Summary = t.TypeOf; type Groupings = t.TypeOf; type Meta = t.TypeOf; type GroupSummary = t.TypeOf; +type GroupBy = t.TypeOf; +type StoredSLOSettings = t.OutputOf; -export type { DateRange, Groupings, HistoricalSummary, Meta, Status, Summary, GroupSummary }; +export type { + Objective, + DateRange, + Groupings, + HistoricalSummary, + Meta, + Status, + Summary, + GroupBy, + GroupSummary, + StoredSLOSettings, +}; diff --git a/x-pack/plugins/observability_solution/slo/server/domain/models/slo.ts b/x-pack/plugins/observability_solution/slo/server/domain/models/slo.ts index d17498c3dbfed..1fbaae2b81706 100644 --- a/x-pack/plugins/observability_solution/slo/server/domain/models/slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/domain/models/slo.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { sloDefinitionSchema, sloIdSchema } from '@kbn/slo-schema'; import * as t from 'io-ts'; -import { sloIdSchema, sloSchema, sloWithSummarySchema } from '@kbn/slo-schema'; -type SLO = t.TypeOf; +type SLODefinition = t.TypeOf; +type StoredSLODefinition = t.OutputOf; + type SLOId = t.TypeOf; -type SLOWithSummary = t.TypeOf; -type StoredSLO = t.OutputOf; -export type { SLO, SLOWithSummary, SLOId, StoredSLO }; +export type { SLODefinition, StoredSLODefinition, SLOId }; diff --git a/x-pack/plugins/observability_solution/slo/server/domain/services/compute_burn_rate.ts b/x-pack/plugins/observability_solution/slo/server/domain/services/compute_burn_rate.ts index 2cd758c4c3a74..78106fd7c3d96 100644 --- a/x-pack/plugins/observability_solution/slo/server/domain/services/compute_burn_rate.ts +++ b/x-pack/plugins/observability_solution/slo/server/domain/services/compute_burn_rate.ts @@ -6,13 +6,13 @@ */ import { toHighPrecision } from '../../utils/number'; -import { IndicatorData, SLO } from '../models'; +import { IndicatorData, SLODefinition } from '../models'; /** * A Burn Rate is computed with the Indicator Data retrieved from a specific lookback period * It tells how fast we are consumming our error budget during a specific period */ -export function computeBurnRate(slo: SLO, sliData: IndicatorData): number { +export function computeBurnRate(slo: SLODefinition, sliData: IndicatorData): number { const { good, total } = sliData; if (total === 0 || good >= total) { return 0; diff --git a/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.test.ts b/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.test.ts index 9060f0e95272c..4eb9d19f236fa 100644 --- a/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.test.ts @@ -6,28 +6,23 @@ */ import { createErrorBudget } from '../../services/fixtures/error_budget'; -import { createSLO } from '../../services/fixtures/slo'; import { computeSummaryStatus } from './compute_summary_status'; describe('ComputeSummaryStatus', () => { it("returns 'NO_DATA' when sliValue is -1", () => { - expect(computeSummaryStatus(createSLO(), -1, createErrorBudget())).toBe('NO_DATA'); + expect(computeSummaryStatus({ target: 0.9 }, -1, createErrorBudget())).toBe('NO_DATA'); }); it("returns 'HEALTHY' when sliValue >= target objective", () => { - expect( - computeSummaryStatus(createSLO({ objective: { target: 0.9 } }), 0.9, createErrorBudget()) - ).toBe('HEALTHY'); + expect(computeSummaryStatus({ target: 0.9 }, 0.9, createErrorBudget())).toBe('HEALTHY'); - expect( - computeSummaryStatus(createSLO({ objective: { target: 0.9 } }), 0.99, createErrorBudget()) - ).toBe('HEALTHY'); + expect(computeSummaryStatus({ target: 0.9 }, 0.99, createErrorBudget())).toBe('HEALTHY'); }); it("returns 'DEGRADING' when sliValue < target objective with some remaining error budget", () => { expect( computeSummaryStatus( - createSLO({ objective: { target: 0.9 } }), + { target: 0.9 }, 0.8, createErrorBudget({ remaining: 0.01, consumed: 0.99 }) ) @@ -37,7 +32,7 @@ describe('ComputeSummaryStatus', () => { it("returns 'VIOLATED' when sliValue < target objective and error budget is consummed", () => { expect( computeSummaryStatus( - createSLO({ objective: { target: 0.9 } }), + { target: 0.9 }, 0.8, createErrorBudget({ remaining: 0, consumed: 1.34 }) ) diff --git a/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.ts b/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.ts index 3aaaffe180b6a..e1872d488c931 100644 --- a/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.ts +++ b/x-pack/plugins/observability_solution/slo/server/domain/services/compute_summary_status.ts @@ -5,14 +5,18 @@ * 2.0. */ -import { ErrorBudget, SLO, Status } from '../models'; +import { ErrorBudget, Objective, Status } from '../models'; -export function computeSummaryStatus(slo: SLO, sliValue: number, errorBudget: ErrorBudget): Status { +export function computeSummaryStatus( + objective: Objective, + sliValue: number, + errorBudget: ErrorBudget +): Status { if (sliValue === -1) { return 'NO_DATA'; } - if (sliValue >= slo.objective.target) { + if (sliValue >= objective.target) { return 'HEALTHY'; } else { return errorBudget.remaining > 0 ? 'DEGRADING' : 'VIOLATED'; diff --git a/x-pack/plugins/observability_solution/slo/server/domain/services/get_delay_in_seconds_from_slo.ts b/x-pack/plugins/observability_solution/slo/server/domain/services/get_delay_in_seconds_from_slo.ts index e6cef17750394..fcc8618e785dd 100644 --- a/x-pack/plugins/observability_solution/slo/server/domain/services/get_delay_in_seconds_from_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/domain/services/get_delay_in_seconds_from_slo.ts @@ -6,9 +6,9 @@ */ import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; -import { SLO } from '../models'; +import { SLODefinition } from '../models'; -export function getDelayInSecondsFromSLO(slo: SLO) { +export function getDelayInSecondsFromSLO(slo: SLODefinition) { const fixedInterval = timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) ? slo.objective.timesliceWindow!.asSeconds() : 60; diff --git a/x-pack/plugins/observability_solution/slo/server/domain/services/validate_slo.ts b/x-pack/plugins/observability_solution/slo/server/domain/services/validate_slo.ts index eb253f44cdf5a..d98e1a0ef40b6 100644 --- a/x-pack/plugins/observability_solution/slo/server/domain/services/validate_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/domain/services/validate_slo.ts @@ -13,16 +13,16 @@ import { calendarAlignedTimeWindowSchema, } from '@kbn/slo-schema'; import { IllegalArgumentError } from '../../errors'; -import { SLO } from '../models'; +import { SLODefinition } from '../models'; /** - * Asserts the SLO is valid from a business invariants point of view. + * Asserts the SLO Definition is valid from a business invariants point of view. * e.g. a 'target' objective requires a number between ]0, 1] * e.g. a 'timeslices' budgeting method requires an objective's timeslice_target to be defined. * - * @param slo {SLO} + * @param slo {SLODefinition} */ -export function validateSLO(slo: SLO) { +export function validateSLO(slo: SLODefinition) { if (!isValidId(slo.id)) { throw new IllegalArgumentError('Invalid id'); } @@ -64,7 +64,7 @@ export function validateSLO(slo: SLO) { validateSettings(slo); } -function validateSettings(slo: SLO) { +function validateSettings(slo: SLODefinition) { if (!isValidFrequencySettings(slo.settings.frequency)) { throw new IllegalArgumentError('Invalid settings.frequency'); } diff --git a/x-pack/plugins/observability_solution/slo/server/lib/collectors/fetcher.ts b/x-pack/plugins/observability_solution/slo/server/lib/collectors/fetcher.ts index 566a5e88a5cfe..d5494452e6fcf 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/collectors/fetcher.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/collectors/fetcher.ts @@ -5,12 +5,12 @@ * 2.0. */ import { CollectorFetchContext } from '@kbn/usage-collection-plugin/server'; -import { StoredSLO } from '../../domain/models'; +import { StoredSLODefinition } from '../../domain/models'; import { SO_SLO_TYPE } from '../../saved_objects'; import { Usage } from './type'; export const fetcher = async (context: CollectorFetchContext) => { - const finder = context.soClient.createPointInTimeFinder({ + const finder = context.soClient.createPointInTimeFinder({ type: SO_SLO_TYPE, perPage: 100, }); diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts index 0d20a40a204c5..74bae62db0078 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; +import { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { publicAlertsClientMock } from '@kbn/alerting-plugin/server/alerts_client/alerts_client.mock'; +import { ObservabilitySloAlert } from '@kbn/alerts-as-data-utils'; import { IBasePath, IUiSettingsClient, @@ -20,35 +24,21 @@ import { savedObjectsClientMock, } from '@kbn/core/server/mocks'; import { ISearchStartSearchSource } from '@kbn/data-plugin/public'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { MockedLogger } from '@kbn/logging-mocks'; -import { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; -import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; -import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; -import { LocatorPublic } from '@kbn/share-plugin/common'; import { AlertsLocatorParams } from '@kbn/observability-plugin/common'; -import { getRuleExecutor } from './executor'; -import { createSLO } from '../../../services/fixtures/slo'; -import { SLO, StoredSLO } from '../../../domain/models'; -import { SharePluginStart } from '@kbn/share-plugin/server'; import { Rule } from '@kbn/alerting-plugin/common'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { - BurnRateAlertState, - BurnRateAlertContext, - BurnRateAllowedActionGroups, - BurnRateRuleParams, - AlertStates, -} from './types'; -import { SLONotFound } from '../../../errors'; -import { SO_SLO_TYPE } from '../../../saved_objects'; -import { sloSchema } from '@kbn/slo-schema'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { SharePluginStart } from '@kbn/share-plugin/server'; +import { sloDefinitionSchema } from '@kbn/slo-schema'; +import { get } from 'lodash'; +import { v4 as uuidv4 } from 'uuid'; import { ALERT_ACTION, ALERT_ACTION_ID, HIGH_PRIORITY_ACTION_ID, SUPPRESSED_PRIORITY_ACTION, } from '../../../../common/constants'; -import { EvaluationBucket } from './lib/evaluate'; import { SLO_ID_FIELD, SLO_INSTANCE_ID_FIELD, @@ -60,6 +50,11 @@ import { ALERT_REASON, SLO_BURN_RATE_RULE_TYPE_ID, } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; +import { SLODefinition, StoredSLODefinition } from '../../../domain/models'; +import { SLONotFound } from '../../../errors'; +import { SO_SLO_TYPE } from '../../../saved_objects'; +import { createSLO } from '../../../services/fixtures/slo'; +import { getRuleExecutor } from './executor'; import { generateAboveThresholdKey, generateBurnRateKey, @@ -68,9 +63,14 @@ import { LONG_WINDOW, SHORT_WINDOW, } from './lib/build_query'; -import { get } from 'lodash'; -import { ObservabilitySloAlert } from '@kbn/alerts-as-data-utils'; -import { publicAlertsClientMock } from '@kbn/alerting-plugin/server/alerts_client/alerts_client.mock'; +import { EvaluationBucket } from './lib/evaluate'; +import { + AlertStates, + BurnRateAlertContext, + BurnRateAlertState, + BurnRateAllowedActionGroups, + BurnRateRuleParams, +} from './types'; const commonEsResponse = { took: 100, @@ -86,14 +86,16 @@ const commonEsResponse = { }, }; -function createFindResponse(sloList: SLO[]): SavedObjectsFindResponse { +function createFindResponse( + sloList: SLODefinition[] +): SavedObjectsFindResponse { return { page: 1, per_page: 25, total: sloList.length, saved_objects: sloList.map((slo) => ({ id: slo.id, - attributes: sloSchema.encode(slo), + attributes: sloDefinitionSchema.encode(slo), type: SO_SLO_TYPE, references: [], score: 1, diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/fixtures/rule.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/fixtures/rule.ts index d200a2da82a27..e0588f8d56ada 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/fixtures/rule.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/fixtures/rule.ts @@ -12,11 +12,11 @@ import { LOW_PRIORITY_ACTION, MEDIUM_PRIORITY_ACTION, } from '../../../../../common/constants'; -import { SLO } from '../../../../domain/models'; +import { SLODefinition } from '../../../../domain/models'; import { BurnRateRuleParams } from '../types'; export function createBurnRateRule( - slo: SLO, + slo: SLODefinition, params: Partial = {} ): BurnRateRuleParams { return { diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts index 6b9842abfab48..642067c00724d 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/build_query.ts @@ -6,7 +6,7 @@ */ import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; -import { Duration, SLO, toDurationUnit } from '../../../../domain/models'; +import { Duration, SLODefinition, toDurationUnit } from '../../../../domain/models'; import { BurnRateRuleParams, WindowSchema } from '../types'; import { getDelayInSecondsFromSLO } from '../../../../domain/services/get_delay_in_seconds_from_slo'; import { getLookbackDateRange } from '../../../../domain/services/get_lookback_date_range'; @@ -56,7 +56,7 @@ function buildWindowAgg( id: string, type: WindowType, threshold: number, - slo: SLO, + slo: SLODefinition, dateRange: { from: Date; to: Date } ) { const aggs = timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) @@ -102,7 +102,7 @@ function buildWindowAgg( function buildWindowAggs( startedAt: Date, - slo: SLO, + slo: SLODefinition, burnRateWindows: BurnRateWindowWithDuration[], delayInSeconds = 0 ) { @@ -157,7 +157,7 @@ function buildEvaluation(burnRateWindows: BurnRateWindowWithDuration[]) { export function buildQuery( startedAt: Date, - slo: SLO, + slo: SLODefinition, params: BurnRateRuleParams, afterKey?: EvaluationAfterKey ) { diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts index ef13974c6a771..fc1cca1707430 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import { get } from 'lodash'; -import { Duration, SLO, toDurationUnit } from '../../../../domain/models'; +import { Duration, SLODefinition, toDurationUnit } from '../../../../domain/models'; import { BurnRateRuleParams } from '../types'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../../common/constants'; import { @@ -64,7 +64,7 @@ export interface EvalutionAggResults { async function queryAllResults( esClient: ElasticsearchClient, - slo: SLO, + slo: SLODefinition, params: BurnRateRuleParams, startedAt: Date, buckets: EvaluationBucket[] = [], @@ -93,7 +93,7 @@ async function queryAllResults( export async function evaluate( esClient: ElasticsearchClient, - slo: SLO, + slo: SLODefinition, params: BurnRateRuleParams, startedAt: Date ) { diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate_dependencies.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate_dependencies.ts index 1a1b93a190e52..a01bc3b253173 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate_dependencies.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/evaluate_dependencies.ts @@ -12,12 +12,12 @@ import { ALL_VALUE } from '@kbn/slo-schema'; import { Dependency } from '../../../../../common/types'; import { KibanaSavedObjectsSLORepository } from '../../../../services'; import { BurnRateRuleParams } from '../types'; -import { SLO } from '../../../../domain/models'; +import { SLODefinition } from '../../../../domain/models'; import { evaluate } from './evaluate'; export interface ActiveRule { rule: Rule; - slo: SLO; + slo: SLODefinition; instanceIdsToSuppress: string[]; suppressAll: boolean; } diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/should_suppress_instance_id.test.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/should_suppress_instance_id.test.ts index 37b5f4bed8a89..e84bb512d9b12 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/should_suppress_instance_id.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/lib/should_suppress_instance_id.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SLO } from '../../../../domain/models'; +import { SLODefinition } from '../../../../domain/models'; import { Rule } from '@kbn/alerting-plugin/common'; import { BurnRateRuleParams } from '../types'; import { shouldSuppressInstanceId } from './should_suppress_instance_id'; @@ -15,7 +15,7 @@ describe('shouldSuppressInstanceId', () => { it('should suppress when supressAll is encountered', () => { const results = [ { - slo: {} as unknown as SLO, + slo: {} as unknown as SLODefinition, rule: {} as unknown as Rule, suppressAll: true, instanceIdsToSuppress: [], @@ -27,7 +27,7 @@ describe('shouldSuppressInstanceId', () => { it('should suppress when instanceId is ALL_VALUE and any instanceId matches', () => { const results = [ { - slo: {} as unknown as SLO, + slo: {} as unknown as SLODefinition, rule: {} as unknown as Rule, suppressAll: false, instanceIdsToSuppress: ['foo'], @@ -38,7 +38,7 @@ describe('shouldSuppressInstanceId', () => { it('should suppress when instanceId is matching the same instanceId in the results', () => { const results = [ { - slo: {} as unknown as SLO, + slo: {} as unknown as SLODefinition, rule: {} as unknown as Rule, suppressAll: false, instanceIdsToSuppress: ['foo'], diff --git a/x-pack/plugins/observability_solution/slo/server/plugin.ts b/x-pack/plugins/observability_solution/slo/server/plugin.ts index ca197b3eb463c..b61d0cc200adc 100644 --- a/x-pack/plugins/observability_solution/slo/server/plugin.ts +++ b/x-pack/plugins/observability_solution/slo/server/plugin.ts @@ -37,6 +37,7 @@ import { registerBurnRateRule } from './lib/rules/register_burn_rate_rule'; import { SloConfig } from '.'; import { registerRoutes } from './routes/register_routes'; import { getSloServerRouteRepository } from './routes/get_slo_server_route_repository'; +import { sloSettings } from './saved_objects/slo_settings'; export type SloPluginSetup = ReturnType; @@ -127,6 +128,7 @@ export class SloPlugin implements Plugin { const { ruleDataService } = plugins.ruleRegistry; core.savedObjects.registerType(slo); + core.savedObjects.registerType(sloSettings); registerBurnRateRule(plugins.alerting, core.http.basePath, this.logger, ruleDataService, { alertsLocator, diff --git a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts index 01026b67068b1..10b9ebe0d0ae6 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts @@ -13,13 +13,14 @@ import { deleteSLOParamsSchema, fetchHistoricalSummaryParamsSchema, findSloDefinitionsParamsSchema, - findSLOParamsSchema, findSLOGroupsParamsSchema, + findSLOParamsSchema, getPreviewDataParamsSchema, getSLOBurnRatesParamsSchema, getSLOInstancesParamsSchema, getSLOParamsSchema, manageSLOParamsSchema, + putSLOSettingsParamsSchema, resetSLOParamsSchema, updateSLOParamsSchema, } from '@kbn/slo-schema'; @@ -32,10 +33,10 @@ import { DeleteSLO, DeleteSLOInstances, FindSLO, + FindSLOGroups, GetSLO, KibanaSavedObjectsSLORepository, UpdateSLO, - FindSLOGroups, } from '../../services'; import { FetchHistoricalSummary } from '../../services/fetch_historical_summary'; import { FindSLODefinitions } from '../../services/find_slo_definitions'; @@ -46,15 +47,17 @@ import { GetSLOInstances } from '../../services/get_slo_instances'; import { DefaultHistoricalSummaryClient } from '../../services/historical_summary_client'; import { ManageSLO } from '../../services/manage_slo'; import { ResetSLO } from '../../services/reset_slo'; +import { SloDefinitionClient } from '../../services/slo_definition_client'; +import { getSloSettings, storeSloSettings } from '../../services/slo_settings'; import { DefaultSummarySearchClient } from '../../services/summary_search_client'; import { DefaultSummaryTransformGenerator } from '../../services/summary_transform_generator/summary_transform_generator'; import { ApmTransactionDurationTransformGenerator, ApmTransactionErrorRateTransformGenerator, - SyntheticsAvailabilityTransformGenerator, HistogramTransformGenerator, KQLCustomTransformGenerator, MetricCustomTransformGenerator, + SyntheticsAvailabilityTransformGenerator, TimesliceMetricTransformGenerator, TransformGenerator, } from '../../services/transform_generators'; @@ -263,18 +266,20 @@ const getSLORoute = createSloServerRoute({ access: 'public', }, params: getSLOParamsSchema, - handler: async ({ context, params, logger }) => { + handler: async ({ request, context, params, logger, dependencies }) => { await assertPlatinumLicense(context); + const spaceId = + (await dependencies.spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const summaryClient = new DefaultSummaryClient(esClient); - const getSLO = new GetSLO(repository, summaryClient); - - const response = await getSLO.execute(params.path.id, params.query); + const defintionClient = new SloDefinitionClient(repository, esClient, logger); + const getSLO = new GetSLO(defintionClient, summaryClient); - return response; + return await getSLO.execute(params.path.id, spaceId, params.query); }, }); @@ -412,12 +417,11 @@ const findSLORoute = createSloServerRoute({ const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); - const summarySearchClient = new DefaultSummarySearchClient(esClient, logger, spaceId); - const findSLO = new FindSLO(repository, summarySearchClient); + const summarySearchClient = new DefaultSummarySearchClient(esClient, soClient, logger, spaceId); - const response = await findSLO.execute(params?.query ?? {}); + const findSLO = new FindSLO(repository, summarySearchClient); - return response; + return await findSLO.execute(params?.query ?? {}); }, }); @@ -432,9 +436,10 @@ const findSLOGroupsRoute = createSloServerRoute({ await assertPlatinumLicense(context); const spaceId = (await dependencies.spaces?.spacesService.getActiveSpace(request))?.id ?? 'default'; + const soClient = (await context.core).savedObjects.client; const coreContext = context.core; const esClient = (await coreContext).elasticsearch.client.asCurrentUser; - const findSLOGroups = new FindSLOGroups(esClient, logger, spaceId); + const findSLOGroups = new FindSLOGroups(esClient, soClient, logger, spaceId); const response = await findSLOGroups.execute(params?.query ?? {}); return response; }, @@ -484,16 +489,11 @@ const fetchHistoricalSummary = createSloServerRoute({ handler: async ({ context, params, logger }) => { await assertPlatinumLicense(context); - const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const historicalSummaryClient = new DefaultHistoricalSummaryClient(esClient); + const fetchSummaryData = new FetchHistoricalSummary(historicalSummaryClient); - const fetchSummaryData = new FetchHistoricalSummary(repository, historicalSummaryClient); - - const response = await fetchSummaryData.execute(params.body); - - return response; + return await fetchSummaryData.execute(params.body); }, }); @@ -549,21 +549,27 @@ const getSloBurnRates = createSloServerRoute({ access: 'internal', }, params: getSLOBurnRatesParamsSchema, - handler: async ({ context, params, logger }) => { + handler: async ({ request, context, params, logger, dependencies }) => { await assertPlatinumLicense(context); + const spaceId = + (await dependencies.spaces?.spacesService.getActiveSpace(request))?.id ?? 'default'; + const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; - const burnRates = await getBurnRates( - params.path.id, - params.body.instanceId, - params.body.windows, - { + const { instanceId, windows, remoteName } = params.body; + const burnRates = await getBurnRates({ + instanceId, + spaceId, + windows, + remoteName, + sloId: params.path.id, + services: { soClient, esClient, logger, - } - ); + }, + }); return { burnRates }; }, }); @@ -587,7 +593,38 @@ const getPreviewData = createSloServerRoute({ }, }); +const getSloSettingsRoute = createSloServerRoute({ + endpoint: 'GET /internal/slo/settings', + options: { + tags: ['access:slo_read'], + access: 'internal', + }, + handler: async ({ context }) => { + await assertPlatinumLicense(context); + + const soClient = (await context.core).savedObjects.client; + return await getSloSettings(soClient); + }, +}); + +const putSloSettings = createSloServerRoute({ + endpoint: 'PUT /internal/slo/settings', + options: { + tags: ['access:slo_write'], + access: 'internal', + }, + params: putSLOSettingsParamsSchema, + handler: async ({ context, params }) => { + await assertPlatinumLicense(context); + + const soClient = (await context.core).savedObjects.client; + return await storeSloSettings(soClient, params.body); + }, +}); + export const sloRouteRepository = { + ...getSloSettingsRoute, + ...putSloSettings, ...createSLORoute, ...inspectSLORoute, ...deleteSLORoute, diff --git a/x-pack/plugins/observability_solution/slo/server/saved_objects/slo.ts b/x-pack/plugins/observability_solution/slo/server/saved_objects/slo.ts index 058596e160fd7..1b594db90ae79 100644 --- a/x-pack/plugins/observability_solution/slo/server/saved_objects/slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/saved_objects/slo.ts @@ -7,17 +7,16 @@ import { SavedObjectMigrationFn, SavedObjectsType } from '@kbn/core-saved-objects-server'; import { SavedObject } from '@kbn/core/server'; +import { StoredSLODefinition } from '../domain/models'; -import { StoredSLO } from '../domain/models'; - -type StoredSLOBefore890 = StoredSLO & { +type StoredSLOBefore890 = StoredSLODefinition & { timeWindow: { duration: string; isRolling?: boolean; isCalendar?: boolean; }; }; -const migrateSlo890: SavedObjectMigrationFn = (doc) => { +const migrateSlo890: SavedObjectMigrationFn = (doc) => { const { timeWindow, ...other } = doc.attributes; return { ...doc, @@ -73,7 +72,7 @@ export const slo: SavedObjectsType = { management: { displayName: 'SLO', importableAndExportable: false, - getTitle(sloSavedObject: SavedObject) { + getTitle(sloSavedObject: SavedObject) { return `SLO: [${sloSavedObject.attributes.name}]`; }, }, diff --git a/x-pack/plugins/observability_solution/slo/server/saved_objects/slo_settings.ts b/x-pack/plugins/observability_solution/slo/server/saved_objects/slo_settings.ts new file mode 100644 index 0000000000000..a9e7c6cdf15c1 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/saved_objects/slo_settings.ts @@ -0,0 +1,37 @@ +/* + * 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 { SavedObjectsType } from '@kbn/core-saved-objects-server'; +import { SavedObject } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; +import { StoredSLOSettings } from '../domain/models'; + +export const SO_SLO_SETTINGS_TYPE = 'slo-settings'; +export const sloSettingsObjectId = (space: string = 'default') => `slo-settings-singleton-${space}`; + +export const sloSettings: SavedObjectsType = { + name: SO_SLO_SETTINGS_TYPE, + hidden: false, + namespaceType: 'multiple-isolated', + modelVersions: {}, + mappings: { + dynamic: false, + properties: {}, + }, + management: { + displayName: i18n.translate('xpack.slo.savedObject.sloSettings.displayName', { + defaultMessage: 'SLO Settings', + }), + importableAndExportable: false, + getTitle(sloSavedObject: SavedObject) { + return i18n.translate('xpack.slo.sloSettings.', { + defaultMessage: 'SLO Settings [id={id}]', + values: { id: sloSavedObject.id }, + }); + }, + }, +}; diff --git a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/create_slo.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/create_slo.test.ts.snap index 151f00f946aa3..4f9bd9ab4d8d4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/create_slo.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/create_slo.test.ts.snap @@ -135,6 +135,7 @@ Array [ }, Object { "set": Object { + "description": "Store the indicator params", "field": "slo.indicator.params", "ignore_failure": true, "value": Object { @@ -180,6 +181,7 @@ Array [ "errorBudgetRemaining": 1, "goodEvents": 0, "isTempDoc": true, + "kibanaUrl": "http://myhost.com/mock-server-basepath", "service": Object { "environment": "irrelevant", "name": "irrelevant", @@ -187,18 +189,27 @@ Array [ "sliValue": -1, "slo": Object { "budgetingMethod": "occurrences", + "createdAt": "2024-01-01T00:00:00.000Z", "description": "irrelevant", "groupBy": "*", + "groupings": Object {}, "id": "unique-id", "indicator": Object { + "params": Object { + "environment": "irrelevant", + "index": "metrics-apm*", + "service": "irrelevant", + "transactionName": "irrelevant", + "transactionType": "irrelevant", + }, "type": "sli.apm.transactionErrorRate", }, "instanceId": "*", "name": "irrelevant", "objective": Object { "target": 0.99, - "timesliceTarget": null, - "timesliceWindow": null, + "timesliceTarget": undefined, + "timesliceWindow": undefined, }, "revision": 1, "tags": Array [], @@ -206,6 +217,7 @@ Array [ "duration": "7d", "type": "rolling", }, + "updatedAt": "2024-01-01T00:00:00.000Z", }, "spaceId": "some-space", "status": "NO_DATA", diff --git a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap index fc97cda2c7c90..d3cefbd8cc4d4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap @@ -333,6 +333,7 @@ exports[`ResetSLO resets all associated resources 8`] = ` }, Object { "set": Object { + "description": "Store the indicator params", "field": "slo.indicator.params", "ignore_failure": true, "value": Object { @@ -467,6 +468,7 @@ exports[`ResetSLO resets all associated resources 11`] = ` "errorBudgetRemaining": 1, "goodEvents": 0, "isTempDoc": true, + "kibanaUrl": "http://myhost.com/mock-server-basepath", "service": Object { "environment": "irrelevant", "name": "irrelevant", @@ -474,18 +476,28 @@ exports[`ResetSLO resets all associated resources 11`] = ` "sliValue": -1, "slo": Object { "budgetingMethod": "occurrences", + "createdAt": "2023-01-01T00:00:00.000Z", "description": "irrelevant", "groupBy": "*", + "groupings": Object {}, "id": "irrelevant", "indicator": Object { + "params": Object { + "environment": "irrelevant", + "index": "metrics-apm*", + "service": "irrelevant", + "threshold": 500, + "transactionName": "irrelevant", + "transactionType": "irrelevant", + }, "type": "sli.apm.transactionDuration", }, "instanceId": "*", "name": "irrelevant", "objective": Object { "target": 0.999, - "timesliceTarget": null, - "timesliceWindow": null, + "timesliceTarget": undefined, + "timesliceWindow": undefined, }, "revision": 1, "tags": Array [ @@ -496,6 +508,7 @@ exports[`ResetSLO resets all associated resources 11`] = ` "duration": "7d", "type": "rolling", }, + "updatedAt": "2023-01-01T00:00:00.000Z", }, "spaceId": "some-space", "status": "NO_DATA", diff --git a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/slo_definition_client.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/slo_definition_client.test.ts.snap new file mode 100644 index 0000000000000..95b3ca924632e --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/slo_definition_client.test.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SLODefinitionClient happy path fetches the SLO Definition from the remote summary index when a remoteName is specified 1`] = ` +Object { + "remote": Object { + "kibanaUrl": "http://myhost.com/mock-server-basepath", + "remoteName": "remote_cluster", + }, + "slo": Object { + "budgetingMethod": "occurrences", + "createdAt": 2024-01-01T00:00:00.000Z, + "description": "irrelevant", + "enabled": true, + "groupBy": "*", + "id": "fixed-id", + "indicator": Object { + "params": Object { + "environment": "irrelevant", + "index": "metrics-apm*", + "service": "irrelevant", + "threshold": 500, + "transactionName": "irrelevant", + "transactionType": "irrelevant", + }, + "type": "sli.apm.transactionDuration", + }, + "name": "irrelevant", + "objective": Object { + "target": 0.999, + "timesliceTarget": undefined, + "timesliceWindow": undefined, + }, + "revision": 1, + "settings": Object { + "frequency": Duration { + "unit": "m", + "value": 1, + }, + "syncDelay": Duration { + "unit": "m", + "value": 1, + }, + }, + "tags": Array [ + "critical", + "k8s", + ], + "timeWindow": Object { + "duration": Duration { + "unit": "d", + "value": 7, + }, + "type": "rolling", + }, + "updatedAt": 2024-01-01T00:00:00.000Z, + "version": 1, + }, +} +`; diff --git a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/summary_search_client.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/summary_search_client.test.ts.snap index 13a91a867affd..c5598500e65c3 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/summary_search_client.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/summary_search_client.test.ts.snap @@ -37,8 +37,8 @@ Object { "results": Array [ Object { "groupings": Object {}, - "id": "slo-one", "instanceId": "*", + "sloId": "slo-one", "summary": Object { "errorBudget": Object { "consumed": 0.4, @@ -52,8 +52,8 @@ Object { }, Object { "groupings": Object {}, - "id": "slo_two", "instanceId": "*", + "sloId": "slo_two", "summary": Object { "errorBudget": Object { "consumed": 0.4, @@ -67,8 +67,8 @@ Object { }, Object { "groupings": Object {}, - "id": "slo-three", "instanceId": "*", + "sloId": "slo-three", "summary": Object { "errorBudget": Object { "consumed": 0.4, @@ -82,8 +82,8 @@ Object { }, Object { "groupings": Object {}, - "id": "slo-five", "instanceId": "*", + "sloId": "slo-five", "summary": Object { "errorBudget": Object { "consumed": 0.4, @@ -97,8 +97,8 @@ Object { }, Object { "groupings": Object {}, - "id": "slo-four", "instanceId": "*", + "sloId": "slo-four", "summary": Object { "errorBudget": Object { "consumed": 0.4, diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts index af081baf5cd2d..d6e8b1a309df8 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts @@ -5,10 +5,10 @@ * 2.0. */ +import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient, IBasePath, Logger } from '@kbn/core/server'; import { ALL_VALUE, CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema'; import { v4 as uuidv4 } from 'uuid'; -import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { getSLOSummaryPipelineId, getSLOSummaryTransformId, @@ -17,7 +17,7 @@ import { SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../common/constants'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; -import { Duration, DurationUnit, SLO } from '../domain/models'; +import { Duration, DurationUnit, SLODefinition } from '../domain/models'; import { validateSLO } from '../domain/services'; import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; @@ -62,7 +62,7 @@ export class CreateSLO { this.esClient.index({ index: SLO_SUMMARY_TEMP_INDEX_NAME, id: `slo-${slo.id}`, - document: createTempSummaryDocument(slo, this.spaceId), + document: createTempSummaryDocument(slo, this.spaceId, this.basePath), refresh: true, }), { logger: this.logger } @@ -103,7 +103,7 @@ export class CreateSLO { const summaryTransform = this.summaryTransformManager.inspect(slo); - const temporaryDoc = createTempSummaryDocument(slo, this.spaceId); + const temporaryDoc = createTempSummaryDocument(slo, this.spaceId, this.basePath); return { pipeline, @@ -114,7 +114,7 @@ export class CreateSLO { }; } - private toSLO(params: CreateSLOParams): SLO { + private toSLO(params: CreateSLOParams): SLODefinition { const now = new Date(); return { ...params, @@ -133,7 +133,7 @@ export class CreateSLO { }; } - private toResponse(slo: SLO): CreateSLOResponse { + private toResponse(slo: SLODefinition): CreateSLOResponse { return { id: slo.id, }; diff --git a/x-pack/plugins/observability_solution/slo/server/services/fetch_historical_summary.ts b/x-pack/plugins/observability_solution/slo/server/services/fetch_historical_summary.ts index 095dbd8a64c38..480231a6838fe 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/fetch_historical_summary.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/fetch_historical_summary.ts @@ -10,30 +10,15 @@ import { FetchHistoricalSummaryResponse, fetchHistoricalSummaryResponseSchema, } from '@kbn/slo-schema'; -import { HistoricalSummaryClient, SLOWithInstanceId } from './historical_summary_client'; -import { SLORepository } from './slo_repository'; +import { HistoricalSummaryClient } from './historical_summary_client'; export class FetchHistoricalSummary { - constructor( - private repository: SLORepository, - private historicalSummaryClient: HistoricalSummaryClient - ) {} + constructor(private historicalSummaryClient: HistoricalSummaryClient) {} public async execute( params: FetchHistoricalSummaryParams ): Promise { - const sloIds = params.list.map((slo) => slo.sloId); - const sloList = await this.repository.findAllByIds(sloIds); - - const list: SLOWithInstanceId[] = params.list - .filter(({ sloId }) => sloList.find((slo) => slo.id === sloId)) - .map(({ sloId, instanceId }) => ({ - sloId, - instanceId, - slo: sloList.find((slo) => slo.id === sloId)!, - })); - - const historicalSummary = await this.historicalSummaryClient.fetch(list); + const historicalSummary = await this.historicalSummaryClient.fetch(params); return fetchHistoricalSummaryResponseSchema.encode(historicalSummary); } diff --git a/x-pack/plugins/observability_solution/slo/server/services/find_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/find_slo.test.ts index 775ca6b404070..cfff12d2f503b 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/find_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/find_slo.test.ts @@ -7,12 +7,12 @@ import { ALL_VALUE, Paginated } from '@kbn/slo-schema'; import { SLO_MODEL_VERSION } from '../../common/constants'; -import { SLO } from '../domain/models'; +import { SLODefinition } from '../domain/models'; import { FindSLO } from './find_slo'; import { createSLO } from './fixtures/slo'; import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks'; import { SLORepository } from './slo_repository'; -import { SLOSummary, SummarySearchClient } from './summary_search_client'; +import { SummaryResult, SummarySearchClient } from './summary_search_client'; describe('FindSLO', () => { let mockRepository: jest.Mocked; @@ -158,14 +158,14 @@ describe('FindSLO', () => { }); }); -function summarySearchResult(slo: SLO): Paginated { +function summarySearchResult(slo: SLODefinition): Paginated { return { total: 1, perPage: 25, page: 1, results: [ { - id: slo.id, + sloId: slo.id, instanceId: slo.groupBy === ALL_VALUE ? ALL_VALUE : 'host-abcde', groupings: {}, summary: { diff --git a/x-pack/plugins/observability_solution/slo/server/services/find_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/find_slo.ts index ea9e59c1e2908..2ea0a3c44a8f9 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/find_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/find_slo.ts @@ -6,10 +6,11 @@ */ import { FindSLOParams, FindSLOResponse, findSLOResponseSchema, Pagination } from '@kbn/slo-schema'; -import { SLO, SLOWithSummary } from '../domain/models'; +import { keyBy } from 'lodash'; +import { SLODefinition } from '../domain/models'; import { IllegalArgumentError } from '../errors'; import { SLORepository } from './slo_repository'; -import { SLOSummary, Sort, SummarySearchClient } from './summary_search_client'; +import { Sort, SummaryResult, SummarySearchClient } from './summary_search_client'; const DEFAULT_PAGE = 1; const DEFAULT_PER_PAGE = 25; @@ -22,34 +23,57 @@ export class FindSLO { ) {} public async execute(params: FindSLOParams): Promise { - const sloSummaryList = await this.summarySearchClient.search( + const summaryResults = await this.summarySearchClient.search( params.kqlQuery ?? '', params.filters ?? '', toSort(params), toPagination(params) ); - const sloList = await this.repository.findAllByIds(sloSummaryList.results.map((slo) => slo.id)); - const sloListWithSummary = mergeSloWithSummary(sloList, sloSummaryList.results); + const localSloDefinitions = await this.repository.findAllByIds( + summaryResults.results + .filter((summaryResult) => !summaryResult.remote) + .map((summaryResult) => summaryResult.sloId) + ); return findSLOResponseSchema.encode({ - page: sloSummaryList.page, - perPage: sloSummaryList.perPage, - total: sloSummaryList.total, - results: sloListWithSummary, + page: summaryResults.page, + perPage: summaryResults.perPage, + total: summaryResults.total, + results: mergeSloWithSummary(localSloDefinitions, summaryResults.results), }); } } -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)!, - instanceId: sloSummary.instanceId, - summary: sloSummary.summary, - groupings: sloSummary.groupings, +function mergeSloWithSummary( + localSloDefinitions: SLODefinition[], + summaryResults: SummaryResult[] +) { + const localSloDefinitionsMap = keyBy(localSloDefinitions, (sloDefinition) => sloDefinition.id); + + const localSummaryList = summaryResults + .filter((summaryResult) => !!localSloDefinitionsMap[summaryResult.sloId]) + .map((summaryResult) => ({ + ...localSloDefinitionsMap[summaryResult.sloId], + instanceId: summaryResult.instanceId, + summary: summaryResult.summary, + groupings: summaryResult.groupings, })); + + const remoteSummaryList = summaryResults + .filter((summaryResult) => !!summaryResult.remote) + .map((summaryResult) => ({ + ...summaryResult.remote!.slo, + instanceId: summaryResult.instanceId, + summary: summaryResult.summary, + groupings: summaryResult.groupings, + remote: { + remoteName: summaryResult.remote!.remoteName, + kibanaUrl: summaryResult.remote!.kibanaUrl, + }, + })); + + return [...localSummaryList, ...remoteSummaryList]; } function toPagination(params: FindSLOParams): Pagination { diff --git a/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts b/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts index d6213f4028ca0..e3e1ce9db2381 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts @@ -5,15 +5,13 @@ * 2.0. */ import { FindSLOGroupsParams, FindSLOGroupsResponse, Pagination } from '@kbn/slo-schema'; -import { ElasticsearchClient } from '@kbn/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { findSLOGroupsResponseSchema } from '@kbn/slo-schema'; import { Logger } from '@kbn/core/server'; +import { getListOfSummaryIndices } from './slo_settings'; import { typedSearch } from '../utils/queries'; import { IllegalArgumentError } from '../errors'; -import { - SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - DEFAULT_SLO_GROUPS_PAGE_SIZE, -} from '../../common/constants'; +import { DEFAULT_SLO_GROUPS_PAGE_SIZE } from '../../common/constants'; import { Status } from '../domain/models'; import { getElasticsearchQueryOrThrow } from './transform_generators'; @@ -43,9 +41,11 @@ interface SliDocument { export class FindSLOGroups { constructor( private esClient: ElasticsearchClient, + private soClient: SavedObjectsClientContract, private logger: Logger, private spaceId: string ) {} + public async execute(params: FindSLOGroupsParams): Promise { const pagination = toPagination(params); const groupBy = params.groupBy; @@ -59,10 +59,12 @@ export class FindSLOGroups { this.logger.error(`Failed to parse filters: ${e.message}`); } + const indices = await getListOfSummaryIndices(this.soClient, this.esClient); + const hasSelectedTags = groupBy === 'slo.tags' && groupsFilter.length > 0; const response = await typedSearch(this.esClient, { - index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + index: indices, size: 0, query: { bool: { @@ -151,6 +153,9 @@ export class FindSLOGroups { const results = response.aggregations?.groupBy?.buckets.reduce((acc, bucket) => { const sliDocument = bucket.worst?.hits?.hits[0]?._source as SliDocument; + if (String(bucket.key).endsWith('.temp') && groupBy === '_index') { + return acc; + } return [ ...acc, { diff --git a/x-pack/plugins/observability_solution/slo/server/services/fixtures/slo.ts b/x-pack/plugins/observability_solution/slo/server/services/fixtures/slo.ts index 455f016866b8c..74a548a2ad4c4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/fixtures/slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/fixtures/slo.ts @@ -10,7 +10,7 @@ import { ALL_VALUE, CreateSLOParams, HistogramIndicator, - sloSchema, + sloDefinitionSchema, SyntheticsAvailabilityIndicator, TimesliceMetricIndicator, } from '@kbn/slo-schema'; @@ -25,8 +25,8 @@ import { Indicator, KQLCustomIndicator, MetricCustomIndicator, - SLO, - StoredSLO, + SLODefinition, + StoredSLODefinition, } from '../../domain/models'; import { SO_SLO_TYPE } from '../../saved_objects'; import { twoMinute } from './duration'; @@ -154,7 +154,7 @@ export const createHistogramIndicator = ( }, }); -const defaultSLO: Omit = { +const defaultSLO: Omit = { name: 'irrelevant', description: 'irrelevant', timeWindow: sevenDaysRolling(), @@ -188,16 +188,16 @@ export const createSLOParams = (params: Partial = {}): CreateSL ...params, }); -export const aStoredSLO = (slo: SLO): SavedObject => { +export const aStoredSLO = (slo: SLODefinition): SavedObject => { return { id: slo.id, - attributes: sloSchema.encode(slo), + attributes: sloDefinitionSchema.encode(slo), type: SO_SLO_TYPE, references: [], }; }; -export const createSLO = (params: Partial = {}): SLO => { +export const createSLO = (params: Partial = {}): SLODefinition => { const now = new Date(); return cloneDeep({ ...defaultSLO, @@ -210,7 +210,9 @@ export const createSLO = (params: Partial = {}): SLO => { }); }; -export const createSLOWithTimeslicesBudgetingMethod = (params: Partial = {}): SLO => { +export const createSLOWithTimeslicesBudgetingMethod = ( + params: Partial = {} +): SLODefinition => { return createSLO({ budgetingMethod: 'timeslices', objective: { @@ -222,7 +224,9 @@ export const createSLOWithTimeslicesBudgetingMethod = (params: Partial = {} }); }; -export const createSLOWithCalendarTimeWindow = (params: Partial = {}): SLO => { +export const createSLOWithCalendarTimeWindow = ( + params: Partial = {} +): SLODefinition => { return createSLO({ timeWindow: weeklyCalendarAligned(), ...params, diff --git a/x-pack/plugins/observability_solution/slo/server/services/get_burn_rates.ts b/x-pack/plugins/observability_solution/slo/server/services/get_burn_rates.ts index 9ce58767a868e..44adaa9eb83bc 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/get_burn_rates.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/get_burn_rates.ts @@ -11,6 +11,7 @@ import { Logger } from '@kbn/core/server'; import { Duration } from '../domain/models'; import { computeBurnRate, computeSLI } from '../domain/services'; import { DefaultSLIClient } from './sli_client'; +import { SloDefinitionClient } from './slo_definition_client'; import { KibanaSavedObjectsSLORepository } from './slo_repository'; interface Services { @@ -24,19 +25,30 @@ interface LookbackWindow { duration: Duration; } -export async function getBurnRates( - sloId: string, - instanceId: string, - windows: LookbackWindow[], - services: Services -) { +export async function getBurnRates({ + sloId, + spaceId, + windows, + instanceId, + remoteName, + services, +}: { + sloId: string; + spaceId: string; + instanceId: string; + remoteName?: string; + windows: LookbackWindow[]; + services: Services; +}) { const { soClient, esClient, logger } = services; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const sliClient = new DefaultSLIClient(esClient); - const slo = await repository.findById(sloId); + const definitionClient = new SloDefinitionClient(repository, esClient, logger); - const sliData = await sliClient.fetchSLIDataFrom(slo, instanceId, windows); + const { slo } = await definitionClient.execute(sloId, spaceId, remoteName); + + const sliData = await sliClient.fetchSLIDataFrom(slo, instanceId, windows, remoteName); return Object.keys(sliData).map((key) => { return { name: key, diff --git a/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts b/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts index 6059be2cdf1c5..de77f1c6744b4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/get_preview_data.ts @@ -41,6 +41,7 @@ interface Options { }; interval: string; instanceId?: string; + remoteName?: string; groupBy?: string; groupings?: Record; } @@ -74,8 +75,12 @@ export class GetPreviewData { const truncatedThreshold = Math.trunc(indicator.params.threshold * 1000); + const index = options.remoteName + ? `${options.remoteName}:${indicator.params.index}` + : indicator.params.index; + const result = await typedSearch(this.esClient, { - index: indicator.params.index, + index, size: 0, query: { bool: { @@ -166,8 +171,12 @@ export class GetPreviewData { if (!!indicator.params.filter) filter.push(getElasticsearchQueryOrThrow(indicator.params.filter)); + const index = options.remoteName + ? `${options.remoteName}:${indicator.params.index}` + : indicator.params.index; + const result = await this.esClient.search({ - index: indicator.params.index, + index, size: 0, query: { bool: { @@ -241,8 +250,12 @@ export class GetPreviewData { this.getGroupingsFilter(options, filter); + const index = options.remoteName + ? `${options.remoteName}:${indicator.params.index}` + : indicator.params.index; + const result = await this.esClient.search({ - index: indicator.params.index, + index, size: 0, query: { bool: { @@ -300,8 +313,12 @@ export class GetPreviewData { ]; this.getGroupingsFilter(options, filter); + const index = options.remoteName + ? `${options.remoteName}:${indicator.params.index}` + : indicator.params.index; + const result = await this.esClient.search({ - index: indicator.params.index, + index, size: 0, query: { bool: { @@ -362,8 +379,12 @@ export class GetPreviewData { this.getGroupingsFilter(options, filter); + const index = options.remoteName + ? `${options.remoteName}:${indicator.params.index}` + : indicator.params.index; + const result = await this.esClient.search({ - index: indicator.params.index, + index, size: 0, query: { bool: { @@ -409,8 +430,12 @@ export class GetPreviewData { this.getGroupingsFilter(options, filter); + const index = options.remoteName + ? `${options.remoteName}:${indicator.params.index}` + : indicator.params.index; + const result = await this.esClient.search({ - index: indicator.params.index, + index, size: 0, query: { bool: { @@ -488,8 +513,12 @@ export class GetPreviewData { terms: { 'monitor.project.id': projects }, }); + const index = options.remoteName + ? `${options.remoteName}:${SYNTHETICS_INDEX_PATTERN}` + : SYNTHETICS_INDEX_PATTERN; + const result = await this.esClient.search({ - index: SYNTHETICS_INDEX_PATTERN, + index, size: 0, query: { bool: { @@ -574,6 +603,7 @@ export class GetPreviewData { instanceId: params.instanceId, range: params.range, groupBy: params.groupBy, + remoteName: params.remoteName, groupings: params.groupings, interval: `${bucketSize}m`, }; diff --git a/x-pack/plugins/observability_solution/slo/server/services/get_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/get_slo.test.ts index 1dec5b4e414aa..816fdc00eff34 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/get_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/get_slo.test.ts @@ -12,16 +12,25 @@ import { GetSLO } from './get_slo'; import { createSummaryClientMock, createSLORepositoryMock } from './mocks'; import { SLORepository } from './slo_repository'; import { SummaryClient } from './summary_client'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { SloDefinitionClient } from './slo_definition_client'; describe('GetSLO', () => { let mockRepository: jest.Mocked; let mockSummaryClient: jest.Mocked; let getSLO: GetSLO; + let defintionClient: SloDefinitionClient; beforeEach(() => { mockRepository = createSLORepositoryMock(); mockSummaryClient = createSummaryClientMock(); - getSLO = new GetSLO(mockRepository, mockSummaryClient); + defintionClient = new SloDefinitionClient( + mockRepository, + elasticsearchServiceMock.createElasticsearchClient(), + loggerMock.create() + ); + getSLO = new GetSLO(defintionClient, mockSummaryClient); }); describe('happy path', () => { @@ -43,7 +52,7 @@ describe('GetSLO', () => { }, }); - const result = await getSLO.execute(slo.id); + const result = await getSLO.execute(slo.id, 'default'); expect(mockRepository.findById).toHaveBeenCalledWith(slo.id); expect(result).toEqual({ diff --git a/x-pack/plugins/observability_solution/slo/server/services/get_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/get_slo.ts index bcc6045203ff7..799aa2ac5d055 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/get_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/get_slo.ts @@ -4,32 +4,38 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { ALL_VALUE, GetSLOParams, GetSLOResponse, getSLOResponseSchema } from '@kbn/slo-schema'; -import { Groupings, Meta, SLO, Summary } from '../domain/models'; -import { SLORepository } from './slo_repository'; +import { SloDefinitionClient } from './slo_definition_client'; import { SummaryClient } from './summary_client'; export class GetSLO { - constructor(private repository: SLORepository, private summaryClient: SummaryClient) {} - - public async execute(sloId: string, params: GetSLOParams = {}): Promise { - const slo = await this.repository.findById(sloId); + constructor( + private definitionClient: SloDefinitionClient, + private summaryClient: SummaryClient + ) {} + public async execute( + sloId: string, + spaceId: string, + params: GetSLOParams = {} + ): Promise { const instanceId = params.instanceId ?? ALL_VALUE; - const { summary, groupings, meta } = await this.summaryClient.computeSummary(slo, instanceId); + const remoteName = params.remoteName; + const { slo, remote } = await this.definitionClient.execute(sloId, spaceId, remoteName); + const { summary, groupings, meta } = await this.summaryClient.computeSummary({ + slo, + instanceId, + remoteName, + }); - return getSLOResponseSchema.encode( - mergeSloWithSummary(slo, summary, instanceId, groupings, meta) - ); + return getSLOResponseSchema.encode({ + ...slo, + instanceId, + summary, + groupings, + meta, + remote, + }); } } - -function mergeSloWithSummary( - slo: SLO, - summary: Summary, - instanceId: string, - groupings: Groupings, - meta: Meta -) { - return { ...slo, instanceId, summary, groupings, meta }; -} diff --git a/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.test.ts b/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.test.ts index d6673010760d3..ee49c439fb5b7 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.test.ts @@ -146,7 +146,19 @@ describe('FetchHistoricalSummary', () => { esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForRollingSLO(30)); const client = new DefaultHistoricalSummaryClient(esClientMock); - const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]); + const results = await client.fetch({ + list: [ + { + timeWindow: slo.timeWindow, + groupBy: slo.groupBy, + budgetingMethod: slo.budgetingMethod, + objective: slo.objective, + revision: slo.revision, + sloId: slo.id, + instanceId: ALL_VALUE, + }, + ], + }); results[0].data.forEach((dailyResult) => expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) @@ -167,7 +179,19 @@ describe('FetchHistoricalSummary', () => { esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForRollingSLO(30)); const client = new DefaultHistoricalSummaryClient(esClientMock); - const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]); + const results = await client.fetch({ + list: [ + { + timeWindow: slo.timeWindow, + groupBy: slo.groupBy, + budgetingMethod: slo.budgetingMethod, + objective: slo.objective, + revision: slo.revision, + sloId: slo.id, + instanceId: ALL_VALUE, + }, + ], + }); results[0].data.forEach((dailyResult) => expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) @@ -189,7 +213,19 @@ describe('FetchHistoricalSummary', () => { esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForMonthlyCalendarAlignedSLO()); const client = new DefaultHistoricalSummaryClient(esClientMock); - const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]); + const results = await client.fetch({ + list: [ + { + timeWindow: slo.timeWindow, + groupBy: slo.groupBy, + budgetingMethod: slo.budgetingMethod, + objective: slo.objective, + revision: slo.revision, + sloId: slo.id, + instanceId: ALL_VALUE, + }, + ], + }); results[0].data.forEach((dailyResult) => expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) @@ -211,7 +247,19 @@ describe('FetchHistoricalSummary', () => { esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForMonthlyCalendarAlignedSLO()); const client = new DefaultHistoricalSummaryClient(esClientMock); - const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]); + const results = await client.fetch({ + list: [ + { + timeWindow: slo.timeWindow, + groupBy: slo.groupBy, + budgetingMethod: slo.budgetingMethod, + objective: slo.objective, + revision: slo.revision, + sloId: slo.id, + instanceId: ALL_VALUE, + }, + ], + }); results[0].data.forEach((dailyResult) => expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) @@ -230,7 +278,19 @@ describe('FetchHistoricalSummary', () => { esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForRollingSLO(30)); const client = new DefaultHistoricalSummaryClient(esClientMock); - const results = await client.fetch([{ slo, sloId: slo.id, instanceId: 'host-abc' }]); + const results = await client.fetch({ + list: [ + { + timeWindow: slo.timeWindow, + groupBy: slo.groupBy, + budgetingMethod: slo.budgetingMethod, + objective: slo.objective, + revision: slo.revision, + sloId: slo.id, + instanceId: 'host-abc', + }, + ], + }); expect( // @ts-ignore diff --git a/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.ts b/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.ts index 36d28b245a602..219afcb22425d 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/historical_summary_client.ts @@ -9,8 +9,11 @@ import { MsearchMultisearchBody } from '@elastic/elasticsearch/lib/api/typesWith import { ElasticsearchClient } from '@kbn/core/server'; import { ALL_VALUE, + BudgetingMethod, calendarAlignedTimeWindowSchema, Duration, + DurationUnit, + FetchHistoricalSummaryParams, fetchHistoricalSummaryResponseSchema, occurrencesBudgetingMethodSchema, rollingTimeWindowSchema, @@ -21,7 +24,14 @@ import { assertNever } from '@kbn/std'; import * as t from 'io-ts'; import moment from 'moment'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants'; -import { DateRange, HistoricalSummary, SLO, SLOId } from '../domain/models'; +import { + DateRange, + GroupBy, + HistoricalSummary, + Objective, + SLOId, + TimeWindow, +} from '../domain/models'; import { computeSLI, computeSummaryStatus, toDateRange, toErrorBudget } from '../domain/services'; interface DailyAggBucket { @@ -42,31 +52,42 @@ interface DailyAggBucket { }; } -export interface SLOWithInstanceId { - sloId: SLOId; - instanceId: string; - slo: SLO; -} - export type HistoricalSummaryResponse = t.TypeOf; export interface HistoricalSummaryClient { - fetch(list: SLOWithInstanceId[]): Promise; + fetch(list: FetchHistoricalSummaryParams): Promise; } export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient { constructor(private esClient: ElasticsearchClient) {} - async fetch(list: SLOWithInstanceId[]): Promise { - const dateRangeBySlo = list.reduce>((acc, { sloId, slo }) => { - acc[sloId] = getDateRange(slo); - return acc; - }, {}); - - const searches = list.flatMap(({ sloId, instanceId, slo }) => [ - { index: SLO_DESTINATION_INDEX_PATTERN }, - generateSearchQuery(slo, instanceId, dateRangeBySlo[sloId]), - ]); + async fetch(params: FetchHistoricalSummaryParams): Promise { + const dateRangeBySlo = params.list.reduce>( + (acc, { sloId, timeWindow }) => { + acc[sloId] = getDateRange(timeWindow); + return acc; + }, + {} + ); + + const searches = params.list.flatMap( + ({ sloId, revision, budgetingMethod, instanceId, groupBy, timeWindow, remoteName }) => [ + { + index: remoteName + ? `${remoteName}:${SLO_DESTINATION_INDEX_PATTERN}` + : SLO_DESTINATION_INDEX_PATTERN, + }, + generateSearchQuery({ + groupBy, + sloId, + revision, + instanceId, + timeWindow, + budgetingMethod, + dateRange: dateRangeBySlo[sloId], + }), + ] + ); const historicalSummary: HistoricalSummaryResponse = []; if (searches.length === 0) { @@ -76,9 +97,9 @@ export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient { const result = await this.esClient.msearch({ searches }); for (let i = 0; i < result.responses.length; i++) { - const { slo, sloId, instanceId } = list[i]; + const { sloId, instanceId, timeWindow, budgetingMethod, objective } = params.list[i]; if ('error' in result.responses[i]) { - // handle errorneous responses with an empty historical summary data + // handle erroneous responses with an empty historical summary data historicalSummary.push({ sloId, instanceId, data: [] }); continue; } @@ -86,40 +107,40 @@ export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient { // @ts-ignore typing msearch is hard, we cast the response to what it is supposed to be. const buckets = (result.responses[i].aggregations?.daily?.buckets as DailyAggBucket[]) || []; - if (rollingTimeWindowSchema.is(slo.timeWindow)) { + if (rollingTimeWindowSchema.is(timeWindow)) { historicalSummary.push({ sloId, instanceId, - data: handleResultForRolling(slo, buckets), + data: handleResultForRolling(objective, timeWindow, buckets), }); continue; } - if (calendarAlignedTimeWindowSchema.is(slo.timeWindow)) { - if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) { + if (calendarAlignedTimeWindowSchema.is(timeWindow)) { + if (timeslicesBudgetingMethodSchema.is(budgetingMethod)) { const dateRange = dateRangeBySlo[sloId]; historicalSummary.push({ sloId, instanceId, - data: handleResultForCalendarAlignedAndTimeslices(slo, buckets, dateRange), + data: handleResultForCalendarAlignedAndTimeslices(objective, buckets, dateRange), }); continue; } - if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) { + if (occurrencesBudgetingMethodSchema.is(budgetingMethod)) { historicalSummary.push({ sloId, instanceId, - data: handleResultForCalendarAlignedAndOccurrences(slo, buckets), + data: handleResultForCalendarAlignedAndOccurrences(objective, buckets), }); continue; } - assertNever(slo.budgetingMethod); + assertNever(budgetingMethod); } - assertNever(slo.timeWindow); + assertNever(timeWindow); } return historicalSummary; @@ -127,10 +148,10 @@ export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient { } function handleResultForCalendarAlignedAndOccurrences( - slo: SLO, + objective: Objective, buckets: DailyAggBucket[] ): HistoricalSummary[] { - const initialErrorBudget = 1 - slo.objective.target; + const initialErrorBudget = 1 - objective.target; return buckets.map((bucket: DailyAggBucket): HistoricalSummary => { const good = bucket.cumulative_good?.value ?? 0; @@ -143,23 +164,23 @@ function handleResultForCalendarAlignedAndOccurrences( date: new Date(bucket.key_as_string), errorBudget, sliValue, - status: computeSummaryStatus(slo, sliValue, errorBudget), + status: computeSummaryStatus(objective, sliValue, errorBudget), }; }); } function handleResultForCalendarAlignedAndTimeslices( - slo: SLO, + objective: Objective, buckets: DailyAggBucket[], dateRange: DateRange ): HistoricalSummary[] { - const initialErrorBudget = 1 - slo.objective.target; + const initialErrorBudget = 1 - objective.target; return buckets.map((bucket: DailyAggBucket): HistoricalSummary => { const good = bucket.cumulative_good?.value ?? 0; const total = bucket.cumulative_total?.value ?? 0; const sliValue = computeSLI(good, total); - const totalSlices = computeTotalSlicesFromDateRange(dateRange, slo.objective.timesliceWindow!); + const totalSlices = computeTotalSlicesFromDateRange(dateRange, objective.timesliceWindow!); const consumedErrorBudget = (total - good) / (totalSlices * initialErrorBudget); const errorBudget = toErrorBudget(initialErrorBudget, consumedErrorBudget); @@ -167,15 +188,19 @@ function handleResultForCalendarAlignedAndTimeslices( date: new Date(bucket.key_as_string), errorBudget, sliValue, - status: computeSummaryStatus(slo, sliValue, errorBudget), + status: computeSummaryStatus(objective, sliValue, errorBudget), }; }); } -function handleResultForRolling(slo: SLO, buckets: DailyAggBucket[]): HistoricalSummary[] { - const initialErrorBudget = 1 - slo.objective.target; +function handleResultForRolling( + objective: Objective, + timeWindow: TimeWindow, + buckets: DailyAggBucket[] +): HistoricalSummary[] { + const initialErrorBudget = 1 - objective.target; const rollingWindowDurationInDays = moment - .duration(slo.timeWindow.duration.value, toMomentUnitOfTime(slo.timeWindow.duration.unit)) + .duration(timeWindow.duration.value, toMomentUnitOfTime(timeWindow.duration.unit)) .asDays(); const { bucketsPerDay } = getFixedIntervalAndBucketsPerDay(rollingWindowDurationInDays); @@ -193,24 +218,36 @@ function handleResultForRolling(slo: SLO, buckets: DailyAggBucket[]): Historical date: new Date(bucket.key_as_string), errorBudget, sliValue, - status: computeSummaryStatus(slo, sliValue, errorBudget), + status: computeSummaryStatus(objective, sliValue, errorBudget), }; }); } -function generateSearchQuery( - slo: SLO, - instanceId: string, - dateRange: DateRange -): MsearchMultisearchBody { - const unit = toMomentUnitOfTime(slo.timeWindow.duration.unit); - const timeWindowDurationInDays = moment.duration(slo.timeWindow.duration.value, unit).asDays(); +function generateSearchQuery({ + sloId, + groupBy, + revision, + instanceId, + dateRange, + timeWindow, + budgetingMethod, +}: { + instanceId: string; + sloId: string; + groupBy: GroupBy; + revision: number; + dateRange: DateRange; + timeWindow: TimeWindow; + budgetingMethod: BudgetingMethod; +}): MsearchMultisearchBody { + const unit = toMomentUnitOfTime(timeWindow.duration.unit); + const timeWindowDurationInDays = moment.duration(timeWindow.duration.value, unit).asDays(); const { fixedInterval, bucketsPerDay } = getFixedIntervalAndBucketsPerDay(timeWindowDurationInDays); const extraFilterByInstanceId = - !!slo.groupBy && ![slo.groupBy].flat().includes(ALL_VALUE) && instanceId !== ALL_VALUE + !!groupBy && ![groupBy].flat().includes(ALL_VALUE) && instanceId !== ALL_VALUE ? [{ term: { 'slo.instanceId': instanceId } }] : []; @@ -219,8 +256,8 @@ function generateSearchQuery( query: { bool: { filter: [ - { term: { 'slo.id': slo.id } }, - { term: { 'slo.revision': slo.revision } }, + { term: { 'slo.id': sloId } }, + { term: { 'slo.revision': revision } }, { range: { '@timestamp': { @@ -244,7 +281,7 @@ function generateSearchQuery( }, }, aggs: { - ...(occurrencesBudgetingMethodSchema.is(slo.budgetingMethod) && { + ...(occurrencesBudgetingMethodSchema.is(budgetingMethod) && { good: { sum: { field: 'slo.numerator', @@ -256,7 +293,7 @@ function generateSearchQuery( }, }, }), - ...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && { + ...(timeslicesBudgetingMethodSchema.is(budgetingMethod) && { good: { sum: { field: 'slo.isGoodSlice', @@ -292,24 +329,24 @@ function generateSearchQuery( }; } -function getDateRange(slo: SLO) { - if (rollingTimeWindowSchema.is(slo.timeWindow)) { - const unit = toMomentUnitOfTime(slo.timeWindow.duration.unit); +function getDateRange(timeWindow: TimeWindow) { + if (rollingTimeWindowSchema.is(timeWindow)) { + const unit = toMomentUnitOfTime(timeWindow.duration.unit as DurationUnit); const now = moment(); return { from: now .clone() - .subtract(slo.timeWindow.duration.value * 2, unit) + .subtract(timeWindow.duration.value * 2, unit) .startOf('day') .toDate(), to: now.startOf('minute').toDate(), }; } - if (calendarAlignedTimeWindowSchema.is(slo.timeWindow)) { - return toDateRange(slo.timeWindow); + if (calendarAlignedTimeWindowSchema.is(timeWindow)) { + return toDateRange(timeWindow); } - assertNever(slo.timeWindow); + assertNever(timeWindow); } function computeTotalSlicesFromDateRange(dateRange: DateRange, timesliceWindow: Duration) { diff --git a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts index ad866fc81c422..f93fce029913f 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts @@ -65,7 +65,7 @@ export class ResetSLO { this.esClient.index({ index: SLO_SUMMARY_TEMP_INDEX_NAME, id: `slo-${slo.id}`, - document: createTempSummaryDocument(slo, this.spaceId), + document: createTempSummaryDocument(slo, this.spaceId, this.basePath), refresh: true, }), { logger: this.logger } diff --git a/x-pack/plugins/observability_solution/slo/server/services/sli_client.ts b/x-pack/plugins/observability_solution/slo/server/services/sli_client.ts index ed645e8da7384..a9345497d4890 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/sli_client.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/sli_client.ts @@ -21,14 +21,14 @@ import { } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants'; -import { DateRange, Duration, IndicatorData, SLO } from '../domain/models'; +import { DateRange, Duration, IndicatorData, SLODefinition } from '../domain/models'; import { InternalQueryError } from '../errors'; import { getDelayInSecondsFromSLO } from '../domain/services/get_delay_in_seconds_from_slo'; import { getLookbackDateRange } from '../domain/services/get_lookback_date_range'; export interface SLIClient { fetchSLIDataFrom( - slo: SLO, + slo: SLODefinition, instanceId: string, lookbackWindows: LookbackWindow[] ): Promise>; @@ -47,9 +47,10 @@ export class DefaultSLIClient implements SLIClient { constructor(private esClient: ElasticsearchClient) {} async fetchSLIDataFrom( - slo: SLO, + slo: SLODefinition, instanceId: string, - lookbackWindows: LookbackWindow[] + lookbackWindows: LookbackWindow[], + remoteName?: string ): Promise> { const sortedLookbackWindows = [...lookbackWindows].sort((a, b) => a.duration.isShorterThan(b.duration) ? 1 : -1 @@ -62,10 +63,14 @@ export class DefaultSLIClient implements SLIClient { delayInSeconds ); + const index = remoteName + ? `${remoteName}:${SLO_DESTINATION_INDEX_PATTERN}` + : SLO_DESTINATION_INDEX_PATTERN; + if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) { const result = await this.esClient.search({ ...commonQuery(slo, instanceId, longestDateRange), - index: SLO_DESTINATION_INDEX_PATTERN, + index, aggs: toLookbackWindowsAggregationsQuery( longestDateRange.to, sortedLookbackWindows, @@ -79,7 +84,7 @@ export class DefaultSLIClient implements SLIClient { if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) { const result = await this.esClient.search({ ...commonQuery(slo, instanceId, longestDateRange), - index: SLO_DESTINATION_INDEX_PATTERN, + index, aggs: toLookbackWindowsSlicedAggregationsQuery( longestDateRange.to, sortedLookbackWindows, @@ -95,7 +100,7 @@ export class DefaultSLIClient implements SLIClient { } function commonQuery( - slo: SLO, + slo: SLODefinition, instanceId: string, dateRange: DateRange ): Pick { diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.test.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.test.ts new file mode 100644 index 0000000000000..485b0c10951ad --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.test.ts @@ -0,0 +1,94 @@ +/* + * 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, + httpServiceMock, + loggingSystemMock, +} from '@kbn/core/server/mocks'; +import { MockedLogger } from '@kbn/logging-mocks'; +import { createSLO } from './fixtures/slo'; +import { createSLORepositoryMock } from './mocks'; +import { SloDefinitionClient } from './slo_definition_client'; +import { SLORepository } from './slo_repository'; +import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; + +describe('SLODefinitionClient', () => { + let esClientMock: ElasticsearchClientMock; + let loggerMock: jest.Mocked; + let mockRepository: jest.Mocked; + let sloDefinitionClient: SloDefinitionClient; + + jest.useFakeTimers().setSystemTime(new Date('2024-01-01')); + + beforeEach(() => { + esClientMock = elasticsearchServiceMock.createElasticsearchClient(); + loggerMock = loggingSystemMock.createLogger(); + mockRepository = createSLORepositoryMock(); + + sloDefinitionClient = new SloDefinitionClient(mockRepository, esClientMock, loggerMock); + }); + + describe('happy path', () => { + it('fetches the SLO Definition from the SLO repository when no remoteName is specified', async () => { + const slo = createSLO({ id: 'fixed-id' }); + mockRepository.findById.mockResolvedValueOnce(slo); + + const response = await sloDefinitionClient.execute('fixed-id', 'default'); + + expect(response).toEqual({ slo }); + }); + + it('fetches the SLO Definition from the remote summary index when a remoteName is specified', async () => { + const slo = createSLO({ id: 'fixed-id' }); + const summaryDoc = createTempSummaryDocument( + slo, + 'default', + httpServiceMock.createStartContract().basePath + ); + esClientMock.search.mockResolvedValueOnce({ + took: 100, + timed_out: false, + _shards: { + total: 0, + successful: 0, + skipped: 0, + failed: 0, + }, + hits: { + hits: [{ _source: summaryDoc, _index: '', _id: '' }], + }, + }); + + const response = await sloDefinitionClient.execute('fixed-id', 'default', 'remote_cluster'); + + expect(response).toMatchSnapshot(); + expect(esClientMock.search.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "index": "remote_cluster:.slo-observability.summary-v3*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "spaceId": "default", + }, + }, + Object { + "term": Object { + "slo.id": "fixed-id", + }, + }, + ], + }, + }, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.ts new file mode 100644 index 0000000000000..39e3d3f446e39 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_definition_client.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants'; +import { SLODefinition } from '../domain/models'; +import { SLORepository } from './slo_repository'; +import { EsSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; +import { fromRemoteSummaryDocumentToSloDefinition } from './unsafe_federated/remote_summary_doc_to_slo'; + +interface SLODefinitionResult { + slo: SLODefinition; + remote?: { + kibanaUrl: string; + remoteName: string; + }; +} + +export class SloDefinitionClient { + constructor( + private repository: SLORepository, + private esClient: ElasticsearchClient, + private logger: Logger + ) {} + + public async execute( + sloId: string, + spaceId: string, + remoteName?: string + ): Promise { + if (remoteName) { + const summarySearch = await this.esClient.search({ + index: `${remoteName}:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}`, + query: { + bool: { + filter: [{ term: { spaceId } }, { term: { 'slo.id': sloId } }], + }, + }, + }); + + if (summarySearch.hits.hits.length === 0) { + throw new Error( + `Remote SLO [id=${sloId}, spaceId=${spaceId}, remoteName=${remoteName}] not found` + ); + } + + const doc = summarySearch.hits.hits[0]._source!; + const remoteSloDefinition = fromRemoteSummaryDocumentToSloDefinition(doc, this.logger); + if (!remoteSloDefinition) { + throw new Error( + `Remote SLO [id=${sloId}, spaceId=${spaceId}, remoteName=${remoteName}] is invalid` + ); + } + + return { slo: remoteSloDefinition, remote: { kibanaUrl: doc.kibanaUrl ?? '', remoteName } }; + } + + const localSloDefinition = await this.repository.findById(sloId); + return { slo: localSloDefinition }; + } +} diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts index 08366a36ccf61..1b6eec0ef4f97 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts @@ -8,9 +8,9 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { MockedLogger } from '@kbn/logging-mocks'; -import { sloSchema } from '@kbn/slo-schema'; +import { sloDefinitionSchema } from '@kbn/slo-schema'; import { SLO_MODEL_VERSION } from '../../common/constants'; -import { SLO, StoredSLO } from '../domain/models'; +import { SLODefinition, StoredSLODefinition } from '../domain/models'; import { SLOIdConflict, SLONotFound } from '../errors'; import { SO_SLO_TYPE } from '../saved_objects'; import { aStoredSLO, createAPMTransactionDurationIndicator, createSLO } from './fixtures/slo'; @@ -21,9 +21,9 @@ const ANOTHER_SLO = createSLO(); const INVALID_SLO_ID = 'invalid-slo-id'; function soFindResponse( - sloList: SLO[], + sloList: SLODefinition[], includeInvalidStoredSLO: boolean = false -): SavedObjectsFindResponse { +): SavedObjectsFindResponse { return { page: 1, per_page: 25, @@ -32,7 +32,7 @@ function soFindResponse( saved_objects: [ ...sloList.map((slo) => ({ id: slo.id, - attributes: sloSchema.encode(slo), + attributes: sloDefinitionSchema.encode(slo), type: SO_SLO_TYPE, references: [], score: 1, @@ -97,10 +97,14 @@ describe('KibanaSavedObjectsSLORepository', () => { perPage: 1, filter: `slo.attributes.id:(${slo.id})`, }); - expect(soClientMock.create).toHaveBeenCalledWith(SO_SLO_TYPE, sloSchema.encode(slo), { - id: undefined, - overwrite: true, - }); + expect(soClientMock.create).toHaveBeenCalledWith( + SO_SLO_TYPE, + sloDefinitionSchema.encode(slo), + { + id: undefined, + overwrite: true, + } + ); }); it('throws when the SLO id already exists and "throwOnConflict" is true', async () => { @@ -134,10 +138,14 @@ describe('KibanaSavedObjectsSLORepository', () => { perPage: 1, filter: `slo.attributes.id:(${slo.id})`, }); - expect(soClientMock.create).toHaveBeenCalledWith(SO_SLO_TYPE, sloSchema.encode(slo), { - id: 'my-id', - overwrite: true, - }); + expect(soClientMock.create).toHaveBeenCalledWith( + SO_SLO_TYPE, + sloDefinitionSchema.encode(slo), + { + id: 'my-id', + overwrite: true, + } + ); }); }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts index 05d690242f0cf..1f8e1d415c484 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts @@ -7,31 +7,31 @@ import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { Logger } from '@kbn/core/server'; -import { ALL_VALUE, Paginated, Pagination, sloSchema } from '@kbn/slo-schema'; +import { ALL_VALUE, Paginated, Pagination, sloDefinitionSchema } from '@kbn/slo-schema'; import { isLeft } from 'fp-ts/lib/Either'; import { SLO_MODEL_VERSION } from '../../common/constants'; -import { SLO, StoredSLO } from '../domain/models'; +import { SLODefinition, StoredSLODefinition } from '../domain/models'; import { SLOIdConflict, SLONotFound } from '../errors'; 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; + save(slo: SLODefinition, options?: { throwOnConflict: boolean }): Promise; + findAllByIds(ids: string[]): Promise; + findById(id: string): Promise; deleteById(id: string): Promise; search( search: string, pagination: Pagination, options?: { includeOutdatedOnly?: boolean } - ): Promise>; + ): Promise>; } export class KibanaSavedObjectsSLORepository implements SLORepository { constructor(private soClient: SavedObjectsClientContract, private logger: Logger) {} - async save(slo: SLO, options = { throwOnConflict: false }): Promise { + async save(slo: SLODefinition, options = { throwOnConflict: false }): Promise { let existingSavedObjectId; - const findResponse = await this.soClient.find({ + const findResponse = await this.soClient.find({ type: SO_SLO_TYPE, page: 1, perPage: 1, @@ -45,7 +45,7 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { existingSavedObjectId = findResponse.saved_objects[0].id; } - await this.soClient.create(SO_SLO_TYPE, toStoredSLO(slo), { + await this.soClient.create(SO_SLO_TYPE, toStoredSLO(slo), { id: existingSavedObjectId, overwrite: true, }); @@ -53,8 +53,8 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { return slo; } - async findById(id: string): Promise { - const response = await this.soClient.find({ + async findById(id: string): Promise { + const response = await this.soClient.find({ type: SO_SLO_TYPE, page: 1, perPage: 1, @@ -74,7 +74,7 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { } async deleteById(id: string): Promise { - const response = await this.soClient.find({ + const response = await this.soClient.find({ type: SO_SLO_TYPE, page: 1, perPage: 1, @@ -88,10 +88,10 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { await this.soClient.delete(SO_SLO_TYPE, response.saved_objects[0].id); } - async findAllByIds(ids: string[]): Promise { + async findAllByIds(ids: string[]): Promise { if (ids.length === 0) return []; - const response = await this.soClient.find({ + const response = await this.soClient.find({ type: SO_SLO_TYPE, page: 1, perPage: ids.length, @@ -100,15 +100,15 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { return response.saved_objects .map((slo) => this.toSLO(slo.attributes)) - .filter((slo) => slo !== undefined) as SLO[]; + .filter((slo) => slo !== undefined) as SLODefinition[]; } async search( search: string, pagination: Pagination, options: { includeOutdatedOnly?: boolean } = { includeOutdatedOnly: false } - ): Promise> { - const response = await this.soClient.find({ + ): Promise> { + const response = await this.soClient.find({ type: SO_SLO_TYPE, page: pagination.page, perPage: pagination.perPage, @@ -125,12 +125,12 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { page: response.page, results: response.saved_objects .map((savedObject) => this.toSLO(savedObject.attributes)) - .filter((slo) => slo !== undefined) as SLO[], + .filter((slo) => slo !== undefined) as SLODefinition[], }; } - toSLO(storedSLO: StoredSLO): SLO | undefined { - const result = sloSchema.decode({ + toSLO(storedSLO: StoredSLODefinition): SLODefinition | undefined { + const result = sloDefinitionSchema.decode({ ...storedSLO, // groupBy was added in 8.10.0 groupBy: storedSLO.groupBy ?? ALL_VALUE, @@ -149,6 +149,6 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { } } -function toStoredSLO(slo: SLO): StoredSLO { - return sloSchema.encode(slo); +function toStoredSLO(slo: SLODefinition): StoredSLODefinition { + return sloDefinitionSchema.encode(slo); } diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_settings.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_settings.ts new file mode 100644 index 0000000000000..407fd692cc646 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_settings.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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { PutSLOSettingsParams, sloSettingsSchema } from '@kbn/slo-schema'; +import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants'; +import { getListOfSloSummaryIndices } from '../../common/summary_indices'; +import { StoredSLOSettings } from '../domain/models'; +import { sloSettingsObjectId, SO_SLO_SETTINGS_TYPE } from '../saved_objects/slo_settings'; + +export const getSloSettings = async (soClient: SavedObjectsClientContract) => { + try { + const soObject = await soClient.get( + SO_SLO_SETTINGS_TYPE, + sloSettingsObjectId(soClient.getCurrentNamespace()) + ); + return sloSettingsSchema.encode(soObject.attributes); + } catch (e) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + return { + useAllRemoteClusters: false, + selectedRemoteClusters: [], + }; + } + throw e; + } +}; + +export const storeSloSettings = async ( + soClient: SavedObjectsClientContract, + params: PutSLOSettingsParams +) => { + const object = await soClient.create( + SO_SLO_SETTINGS_TYPE, + sloSettingsSchema.encode(params), + { + id: sloSettingsObjectId(soClient.getCurrentNamespace()), + overwrite: true, + } + ); + + return sloSettingsSchema.encode(object.attributes); +}; + +export const getListOfSummaryIndices = async ( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient +) => { + const indices: string[] = [SLO_SUMMARY_DESTINATION_INDEX_PATTERN]; + + const settings = await getSloSettings(soClient); + const { useAllRemoteClusters, selectedRemoteClusters } = settings; + if (!useAllRemoteClusters && selectedRemoteClusters.length === 0) { + return indices; + } + + const clustersByName = await esClient.cluster.remoteInfo(); + const clusterNames = (clustersByName && Object.keys(clustersByName)) || []; + const clusterInfo = clusterNames.map((clusterName) => ({ + name: clusterName, + isConnected: clustersByName[clusterName].connected, + })); + + return getListOfSloSummaryIndices(settings, clusterInfo); +}; diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts index 15388b221cc74..968687b1bd569 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts @@ -54,7 +54,7 @@ describe('SummaryClient', () => { esClientMock.msearch.mockResolvedValueOnce(createEsResponse()); const summaryClient = new DefaultSummaryClient(esClientMock); - const result = await summaryClient.computeSummary(slo); + const result = await summaryClient.computeSummary({ slo }); expect(result).toMatchSnapshot(); expect(esClientMock.search.mock.calls[0][0]).toEqual({ @@ -89,7 +89,7 @@ describe('SummaryClient', () => { esClientMock.msearch.mockResolvedValueOnce(createEsResponse()); const summaryClient = new DefaultSummaryClient(esClientMock); - await summaryClient.computeSummary(slo); + await summaryClient.computeSummary({ slo }); expect(esClientMock.search.mock.calls[0][0]).toEqual({ index: SLO_DESTINATION_INDEX_PATTERN, @@ -132,7 +132,7 @@ describe('SummaryClient', () => { esClientMock.msearch.mockResolvedValueOnce(createEsResponse()); const summaryClient = new DefaultSummaryClient(esClientMock); - const result = await summaryClient.computeSummary(slo); + const result = await summaryClient.computeSummary({ slo }); expect(result).toMatchSnapshot(); expect(esClientMock.search.mock.calls[0][0]).toEqual({ @@ -181,7 +181,7 @@ describe('SummaryClient', () => { esClientMock.msearch.mockResolvedValueOnce(createEsResponse()); const summaryClient = new DefaultSummaryClient(esClientMock); - const result = await summaryClient.computeSummary(slo); + const result = await summaryClient.computeSummary({ slo }); expect(result).toMatchSnapshot(); diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts index 74f78929a45c8..df6bdfbe944c9 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts @@ -16,31 +16,41 @@ import { } from '@kbn/slo-schema'; import moment from 'moment'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants'; -import { DateRange, SLO, Summary, Groupings, Meta } from '../domain/models'; +import { DateRange, Groupings, Meta, SLODefinition, Summary } from '../domain/models'; import { computeSLI, computeSummaryStatus, toErrorBudget } from '../domain/services'; import { toDateRange } from '../domain/services/date_range'; import { getFlattenedGroupings } from './utils'; +interface Params { + slo: SLODefinition; + instanceId?: string; + remoteName?: string; +} + +interface SummaryResult { + summary: Summary; + groupings: Groupings; + meta: Meta; +} + +// This is called "SummaryClient" but is responsible for: +// - computing summary +// - formatting groupings +// - adding extra Meta parameter for synthetics export interface SummaryClient { - computeSummary( - slo: SLO, - groupings?: string, - instanceId?: string - ): Promise<{ summary: Summary; groupings: Groupings; meta: Meta }>; + computeSummary(params: Params): Promise; } export class DefaultSummaryClient implements SummaryClient { constructor(private esClient: ElasticsearchClient) {} - async computeSummary( - slo: SLO, - instanceId: string = ALL_VALUE - ): Promise<{ summary: Summary; groupings: Groupings; meta: Meta }> { + async computeSummary({ slo, instanceId, remoteName }: Params): Promise { const dateRange = toDateRange(slo.timeWindow); const isDefinedWithGroupBy = ![slo.groupBy].flat().includes(ALL_VALUE); const hasInstanceId = instanceId !== ALL_VALUE; - const includeInstanceIdQueries = isDefinedWithGroupBy && hasInstanceId; - const extraInstanceIdFilter = includeInstanceIdQueries + const shouldIncludeInstanceIdFilter = isDefinedWithGroupBy && hasInstanceId; + + const instanceIdFilter = shouldIncludeInstanceIdFilter ? [{ term: { 'slo.instanceId': instanceId } }] : []; const extraGroupingsAgg = { @@ -62,7 +72,9 @@ export class DefaultSummaryClient implements SummaryClient { }; const result = await this.esClient.search({ - index: SLO_DESTINATION_INDEX_PATTERN, + index: remoteName + ? `${remoteName}:${SLO_DESTINATION_INDEX_PATTERN}` + : SLO_DESTINATION_INDEX_PATTERN, size: 0, query: { bool: { @@ -74,13 +86,13 @@ export class DefaultSummaryClient implements SummaryClient { '@timestamp': { gte: dateRange.from.toISOString(), lt: dateRange.to.toISOString() }, }, }, - ...extraInstanceIdFilter, + ...instanceIdFilter, ], }, }, // @ts-expect-error AggregationsAggregationContainer needs to be updated with top_hits aggs: { - ...(includeInstanceIdQueries && extraGroupingsAgg), + ...(shouldIncludeInstanceIdFilter && extraGroupingsAgg), ...(timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) && { good: { sum: { field: 'slo.isGoodSlice' }, @@ -133,10 +145,10 @@ export class DefaultSummaryClient implements SummaryClient { summary: { sliValue, errorBudget, - status: computeSummaryStatus(slo, sliValue, errorBudget), + status: computeSummaryStatus(slo.objective, sliValue, errorBudget), }, groupings: groupings ? getFlattenedGroupings({ groupBy: slo.groupBy, groupings }) : {}, - meta: getMetaFields(slo, source || {}), + meta: getMetaFields(slo, source ?? {}), }; } } @@ -149,8 +161,8 @@ function computeTotalSlicesFromDateRange(dateRange: DateRange, timesliceWindow: return Math.ceil(dateRangeDurationInUnit / timesliceWindow!.value); } -export function getMetaFields( - slo: SLO, +function getMetaFields( + slo: SLODefinition, source: { monitor?: { id?: string }; config_id?: string; observer?: { name?: string } } ): Meta { const { @@ -160,9 +172,9 @@ export function getMetaFields( case 'sli.synthetics.availability': return { synthetics: { - monitorId: source.monitor?.id || '', - locationId: source.observer?.name || '', - configId: source.config_id || '', + monitorId: source.monitor?.id ?? '', + locationId: source.observer?.name ?? '', + configId: source.config_id ?? '', }, }; default: diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.test.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.test.ts index 3743cbea46e06..bb25e717b6747 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.test.ts @@ -30,7 +30,21 @@ describe('Summary Search Client', () => { beforeEach(() => { esClientMock = elasticsearchServiceMock.createElasticsearchClient(); - service = new DefaultSummarySearchClient(esClientMock, loggerMock.create(), 'some-space'); + const soClientMock = { + getCurrentNamespace: jest.fn().mockReturnValue('default'), + get: jest.fn().mockResolvedValue({ + attributes: { + selectedRemoteClusters: [], + useAllRemoteClusters: false, + }, + }), + } as any; + service = new DefaultSummarySearchClient( + esClientMock, + soClientMock, + loggerMock.create(), + 'some-space' + ); }); it('returns an empty response on error', async () => { diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.ts index cea18773777ae..e5f9992cc0f21 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_search_client.ts @@ -5,43 +5,35 @@ * 2.0. */ -import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; +import { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; import { partition } from 'lodash'; -import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants'; -import { SLOId, Status, Summary, Groupings } from '../domain/models'; +import { Groupings, SLODefinition, SLOId, Summary } from '../domain/models'; import { toHighPrecision } from '../utils/number'; -import { getFlattenedGroupings } from './utils'; +import { createEsParams, typedSearch } from '../utils/queries'; +import { getListOfSummaryIndices } from './slo_settings'; +import { EsSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { getElasticsearchQueryOrThrow } from './transform_generators'; +import { fromRemoteSummaryDocumentToSloDefinition } from './unsafe_federated/remote_summary_doc_to_slo'; +import { getFlattenedGroupings } from './utils'; -interface EsSummaryDocument { - slo: { - id: string; - revision: number; - instanceId: string; - groupings: Groupings; - groupBy: string[]; - }; - sliValue: number; - errorBudgetConsumed: number; - errorBudgetRemaining: number; - errorBudgetInitial: number; - errorBudgetEstimated: boolean; - statusCode: number; - status: Status; - isTempDoc: boolean; -} - -export interface SLOSummary { - id: SLOId; +export interface SummaryResult { + sloId: SLOId; instanceId: string; summary: Summary; groupings: Groupings; + remote?: { + kibanaUrl: string; + remoteName: string; + slo: SLODefinition; + }; } -export type SortField = 'error_budget_consumed' | 'error_budget_remaining' | 'sli_value' | 'status'; +type SortField = 'error_budget_consumed' | 'error_budget_remaining' | 'sli_value' | 'status'; + export interface Sort { field: SortField; direction: 'asc' | 'desc'; @@ -53,12 +45,13 @@ export interface SummarySearchClient { filters: string, sort: Sort, pagination: Pagination - ): Promise>; + ): Promise>; } export class DefaultSummarySearchClient implements SummarySearchClient { constructor( private esClient: ElasticsearchClient, + private soClient: SavedObjectsClientContract, private logger: Logger, private spaceId: string ) {} @@ -68,7 +61,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient { filters: string, sort: Sort, pagination: Pagination - ): Promise> { + ): Promise> { let parsedFilters: any = {}; try { @@ -77,32 +70,38 @@ export class DefaultSummarySearchClient implements SummarySearchClient { this.logger.error(`Failed to parse filters: ${e.message}`); } - try { - const summarySearch = await this.esClient.search({ - index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - track_total_hits: true, - query: { - bool: { - filter: [ - { term: { spaceId: this.spaceId } }, - getElasticsearchQueryOrThrow(kqlQuery), - ...(parsedFilters.filter ?? []), - ], - must_not: [...(parsedFilters.must_not ?? [])], - }, + const indices = await getListOfSummaryIndices(this.soClient, this.esClient); + const esParams = createEsParams({ + index: indices, + track_total_hits: true, + query: { + bool: { + filter: [ + { term: { spaceId: this.spaceId } }, + getElasticsearchQueryOrThrow(kqlQuery), + ...(parsedFilters.filter ?? []), + ], + must_not: [...(parsedFilters.must_not ?? [])], + }, + }, + sort: { + // non-temp first, then temp documents + isTempDoc: { + order: 'asc', }, - sort: { - // non-temp first, then temp documents - isTempDoc: { - order: 'asc', - }, - [toDocumentSortField(sort.field)]: { - order: sort.direction, - }, + [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 - }); + }, + 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 + }); + + try { + const summarySearch = await typedSearch( + this.esClient, + esParams + ); const total = (summarySearch.hits.total as SearchTotalHits).value ?? 0; if (total === 0) { @@ -114,21 +113,12 @@ export class DefaultSummarySearchClient implements SummarySearchClient { (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 } }], - }, - }, - }); + // TODO filter out remote summary documents from the deletion of outdated summaries + const summarySloIds = summaryDocuments.map((doc) => doc._source.slo.id); + await this.deleteOutdatedTemporarySummaries(summarySloIds); const tempSummaryDocumentsDeduped = tempSummaryDocuments.filter( - (doc) => !summarySloIds.includes(doc._source?.slo.id) + (doc) => !summarySloIds.includes(doc._source.slo.id) ); const finalResults = summaryDocuments @@ -138,33 +128,71 @@ export class DefaultSummarySearchClient implements SummarySearchClient { const finalTotal = total - (tempSummaryDocuments.length - tempSummaryDocumentsDeduped.length); return { + ...pagination, total: finalTotal, - perPage: pagination.perPage, - page: pagination.page, - results: finalResults.map((doc) => ({ - id: doc._source!.slo.id, - instanceId: doc._source!.slo.instanceId ?? ALL_VALUE, - summary: { - errorBudget: { - initial: toHighPrecision(doc._source!.errorBudgetInitial), - consumed: toHighPrecision(doc._source!.errorBudgetConsumed), - remaining: toHighPrecision(doc._source!.errorBudgetRemaining), - isEstimated: doc._source!.errorBudgetEstimated, + results: finalResults.map((doc) => { + const summaryDoc = doc._source; + const remoteName = getRemoteClusterName(doc._index); + const isRemote = !!remoteName; + let remoteSloDefinition; + if (isRemote) { + remoteSloDefinition = fromRemoteSummaryDocumentToSloDefinition(summaryDoc, this.logger); + } + + return { + ...(isRemote && + !!remoteSloDefinition && { + remote: { + kibanaUrl: summaryDoc.kibanaUrl ?? '', + remoteName, + slo: remoteSloDefinition, + }, + }), + sloId: summaryDoc.slo.id, + instanceId: summaryDoc.slo.instanceId ?? ALL_VALUE, + summary: { + errorBudget: { + initial: toHighPrecision(summaryDoc.errorBudgetInitial), + consumed: toHighPrecision(summaryDoc.errorBudgetConsumed), + remaining: toHighPrecision(summaryDoc.errorBudgetRemaining), + isEstimated: summaryDoc.errorBudgetEstimated, + }, + sliValue: toHighPrecision(doc._source.sliValue), + status: summaryDoc.status, }, - sliValue: toHighPrecision(doc._source!.sliValue), - status: doc._source!.status, - }, - groupings: getFlattenedGroupings({ - groupings: doc._source!.slo.groupings, - groupBy: doc._source!.slo.groupBy, - }), - })), + groupings: getFlattenedGroupings({ + groupings: summaryDoc.slo.groupings, + groupBy: summaryDoc.slo.groupBy, + }), + }; + }), }; } catch (err) { - this.logger.error(new Error('Summary search query error', { cause: err })); + this.logger.error(new Error(`Summary search query error, ${err.message}`, { cause: err })); return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] }; } } + + private async deleteOutdatedTemporarySummaries(summarySloIds: string[]) { + // 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 + + await this.esClient.deleteByQuery({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + wait_for_completion: false, + query: { + bool: { + filter: [{ terms: { 'slo.id': summarySloIds } }, { term: { isTempDoc: true } }], + }, + }, + }); + } +} + +function getRemoteClusterName(index: string) { + if (index.includes(':')) { + return index.split(':')[0]; + } } function toDocumentSortField(field: SortField) { diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/common.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/common.ts index 92beb0c071a5b..398e54698f90c 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/common.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/common.ts @@ -6,9 +6,9 @@ */ import { ALL_VALUE } from '@kbn/slo-schema'; -import { SLO } from '../../../domain/models/slo'; +import { SLODefinition } from '../../../domain/models/slo'; -export const getGroupBy = (slo: SLO) => { +export const getGroupBy = (slo: SLODefinition) => { const groups = [slo.groupBy].flat().filter((group) => !!group); const groupings = diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/occurrences.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/occurrences.ts index 3aa871984239c..88d83a486d6c0 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/occurrences.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/occurrences.ts @@ -6,7 +6,7 @@ */ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { SLO } from '../../../domain/models'; +import { SLODefinition } from '../../../domain/models'; import { getSLOSummaryPipelineId, getSLOSummaryTransformId, @@ -16,7 +16,9 @@ import { } from '../../../../common/constants'; import { getGroupBy } from './common'; -export function generateSummaryTransformForOccurrences(slo: SLO): TransformPutTransformRequest { +export function generateSummaryTransformForOccurrences( + slo: SLODefinition +): TransformPutTransformRequest { return { transform_id: getSLOSummaryTransformId(slo.id, slo.revision), dest: { diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_calendar_aligned.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_calendar_aligned.ts index 8a1c15b234fd7..aefdb68e1c710 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_calendar_aligned.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_calendar_aligned.ts @@ -6,7 +6,7 @@ */ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { DurationUnit, SLO } from '../../../domain/models'; +import { DurationUnit, SLODefinition } from '../../../domain/models'; import { getSLOSummaryPipelineId, getSLOSummaryTransformId, @@ -17,7 +17,7 @@ import { import { getGroupBy } from './common'; export function generateSummaryTransformForTimeslicesAndCalendarAligned( - slo: SLO + slo: SLODefinition ): TransformPutTransformRequest { const isWeeklyAligned = slo.timeWindow.duration.unit === DurationUnit.Week; const sliceDurationInSeconds = slo.objective.timesliceWindow!.asSeconds(); diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_rolling.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_rolling.ts index 781131b5c0959..f92d8fb5c0ead 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_rolling.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/generators/timeslices_rolling.ts @@ -6,7 +6,7 @@ */ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { SLO } from '../../../domain/models'; +import { SLODefinition } from '../../../domain/models'; import { getSLOSummaryPipelineId, getSLOSummaryTransformId, @@ -17,7 +17,7 @@ import { import { getGroupBy } from './common'; export function generateSummaryTransformForTimeslicesAndRolling( - slo: SLO + slo: SLODefinition ): TransformPutTransformRequest { return { transform_id: getSLOSummaryTransformId(slo.id, slo.revision), diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/helpers/create_temp_summary.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/helpers/create_temp_summary.ts index 635577166466f..4a2a8791bc008 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/helpers/create_temp_summary.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/helpers/create_temp_summary.ts @@ -5,13 +5,59 @@ * 2.0. */ -import { ALL_VALUE } from '@kbn/slo-schema'; -import { SLO } from '../../../domain/models'; +import { IBasePath } from '@kbn/core-http-server'; +import { ALL_VALUE, BudgetingMethod, Objective, timeWindowSchema } from '@kbn/slo-schema'; +import * as t from 'io-ts'; +import { Indicator, IndicatorTypes, SLODefinition, Status } from '../../../domain/models'; -export function createTempSummaryDocument(slo: SLO, spaceId: string) { +export interface EsSummaryDocument { + service: { + environment: string | null; + name: string | null; + }; + transaction: { + name: string | null; + type: string | null; + }; + slo: { + // >= 8.14: Add indicator.params on the temporary summary as well as real summary through summary pipeline + indicator: { type: IndicatorTypes } | Indicator; + timeWindow: t.OutputOf; + groupBy: string | string[]; + groupings: Record; + instanceId: string; + name: string; + description: string; + id: string; + budgetingMethod: BudgetingMethod; + revision: number; + objective: Objective; + tags: string[]; + createdAt?: string; // >= 8.14 + updatedAt?: string; // >= 8.14 + }; + goodEvents: number; + totalEvents: number; + errorBudgetEstimated: boolean; + errorBudgetRemaining: number; + errorBudgetConsumed: number; + errorBudgetInitial: number; + sliValue: number; + statusCode: number; + status: Status; + isTempDoc: boolean; + spaceId: string; + kibanaUrl?: string; // >= 8.14 +} + +export function createTempSummaryDocument( + slo: SLODefinition, + spaceId: string, + basePath: IBasePath +): EsSummaryDocument { const apmParams = 'environment' in slo.indicator.params ? slo.indicator.params : null; - return { + const doc = { service: { environment: apmParams?.environment ?? null, name: apmParams?.service ?? null, @@ -21,14 +67,14 @@ export function createTempSummaryDocument(slo: SLO, spaceId: string) { type: apmParams?.transactionType ?? null, }, slo: { - indicator: { - type: slo.indicator.type, - }, + // 8.14 adds indicator.params through transform summary pipeline, i.e. indicator.params might be undefined + indicator: slo.indicator, timeWindow: { duration: slo.timeWindow.duration.format(), type: slo.timeWindow.type, }, groupBy: !!slo.groupBy ? slo.groupBy : ALL_VALUE, + groupings: {}, instanceId: ALL_VALUE, name: slo.name, description: slo.description, @@ -37,10 +83,12 @@ export function createTempSummaryDocument(slo: SLO, spaceId: string) { revision: slo.revision, objective: { target: slo.objective.target, - timesliceTarget: slo.objective.timesliceTarget ?? null, - timesliceWindow: slo.objective.timesliceWindow?.format() ?? null, + timesliceTarget: slo.objective.timesliceTarget ?? undefined, + timesliceWindow: slo.objective.timesliceWindow?.format() ?? undefined, }, tags: slo.tags, + createdAt: slo.createdAt.toISOString(), // added in 8.14, i.e. might be undefined + updatedAt: slo.updatedAt.toISOString(), // added in 8.14, i.e. might be undefined }, goodEvents: 0, totalEvents: 0, @@ -50,8 +98,11 @@ export function createTempSummaryDocument(slo: SLO, spaceId: string) { errorBudgetInitial: 1 - slo.objective.target, sliValue: -1, statusCode: 0, - status: 'NO_DATA', + status: 'NO_DATA' as const, isTempDoc: true, spaceId, + kibanaUrl: basePath.publicBaseUrl ?? '', // added in 8.14, i.e. might be undefined }; + + return doc; } diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/summary_transform_generator.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/summary_transform_generator.ts index 7eb63e59718c5..253f1af9972f4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/summary_transform_generator.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_transform_generator/summary_transform_generator.ts @@ -6,17 +6,17 @@ */ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { SLO } from '../../domain/models'; +import { SLODefinition } from '../../domain/models'; import { generateSummaryTransformForOccurrences } from './generators/occurrences'; -import { generateSummaryTransformForTimeslicesAndRolling } from './generators/timeslices_rolling'; import { generateSummaryTransformForTimeslicesAndCalendarAligned } from './generators/timeslices_calendar_aligned'; +import { generateSummaryTransformForTimeslicesAndRolling } from './generators/timeslices_rolling'; export interface SummaryTransformGenerator { - generate(slo: SLO): TransformPutTransformRequest; + generate(slo: SLODefinition): TransformPutTransformRequest; } export class DefaultSummaryTransformGenerator implements SummaryTransformGenerator { - public generate(slo: SLO): TransformPutTransformRequest { + public generate(slo: SLODefinition): TransformPutTransformRequest { if (slo.budgetingMethod === 'occurrences') { return generateSummaryTransformForOccurrences(slo); } else if (slo.budgetingMethod === 'timeslices' && slo.timeWindow.type === 'rolling') { diff --git a/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts b/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts index c9590e5cc90ca..27276f59330e8 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summay_transform_manager.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { ElasticsearchClient, Logger } from '@kbn/core/server'; - import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; -import { SLO } from '../domain/models'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { SLODefinition } from '../domain/models'; import { SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; import { SummaryTransformGenerator } from './summary_transform_generator/summary_transform_generator'; @@ -23,7 +22,7 @@ export class DefaultSummaryTransformManager implements TransformManager { private logger: Logger ) {} - async install(slo: SLO): Promise { + async install(slo: SLODefinition): Promise { const transformParams = this.generator.generate(slo); try { await retryTransientEsErrors(() => this.esClient.transform.putTransform(transformParams), { @@ -41,7 +40,7 @@ export class DefaultSummaryTransformManager implements TransformManager { return transformParams.transform_id; } - inspect(slo: SLO): TransformPutTransformRequest { + inspect(slo: SLODefinition): TransformPutTransformRequest { return this.generator.generate(slo); } diff --git a/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts b/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts index d41ec6e142080..1d9a968dc1ff4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts @@ -13,7 +13,7 @@ import { } from '@kbn/task-manager-plugin/server'; import { AggregationsCompositeAggregateKey } from '@elastic/elasticsearch/lib/api/types'; import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants'; -import { StoredSLO } from '../../domain/models'; +import { StoredSLODefinition } from '../../domain/models'; import { SO_SLO_TYPE } from '../../saved_objects'; import { SloConfig } from '../..'; import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/constants'; @@ -201,7 +201,7 @@ export class SloOrphanSummaryCleanupTask { }; findSloDefinitions = async (ids: string[]) => { - const sloDefinitions = await this.soClient?.find>({ + const sloDefinitions = await this.soClient?.find>({ type: SO_SLO_TYPE, page: 1, perPage: ids.length, diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts index 357bd9b62b624..385025017044d 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts @@ -5,14 +5,14 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALL_VALUE, apmTransactionDurationIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; -import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { estypes } from '@elastic/elasticsearch'; import { getElasticsearchQueryOrThrow, TransformGenerator } from '.'; import { getSLOTransformId, @@ -20,12 +20,12 @@ import { SLO_INGEST_PIPELINE_NAME, } from '../../../common/constants'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; -import { APMTransactionDurationIndicator, SLO } from '../../domain/models'; +import { APMTransactionDurationIndicator, SLODefinition } from '../../domain/models'; import { InvalidTransformError } from '../../errors'; import { parseIndex } from './common'; export class ApmTransactionDurationTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLO): TransformPutTransformRequest { + public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { if (!apmTransactionDurationIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -42,11 +42,11 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator ); } - private buildTransformId(slo: SLO): string { + private buildTransformId(slo: SLODefinition): string { return getSLOTransformId(slo.id, slo.revision); } - private buildGroupBy(slo: SLO, indicator: APMTransactionDurationIndicator) { + private buildGroupBy(slo: SLODefinition, 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, @@ -69,7 +69,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); } - private buildSource(slo: SLO, indicator: APMTransactionDurationIndicator) { + private buildSource(slo: SLODefinition, indicator: APMTransactionDurationIndicator) { const queryFilter: estypes.QueryDslQueryContainer[] = [ { range: { @@ -140,7 +140,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator } private buildAggregations( - slo: SLO, + slo: SLODefinition, indicator: APMTransactionDurationIndicator ): Record { // threshold is in ms (milliseconds), but apm data is stored in us (microseconds) diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts index 384a05358cd3b..8d91a1226d61c 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts @@ -19,12 +19,12 @@ import { SLO_INGEST_PIPELINE_NAME, } from '../../../common/constants'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; -import { APMTransactionErrorRateIndicator, SLO } from '../../domain/models'; +import { APMTransactionErrorRateIndicator, SLODefinition } from '../../domain/models'; import { InvalidTransformError } from '../../errors'; import { parseIndex } from './common'; export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLO): TransformPutTransformRequest { + public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { if (!apmTransactionErrorRateIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -41,11 +41,11 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato ); } - private buildTransformId(slo: SLO): string { + private buildTransformId(slo: SLODefinition): string { return getSLOTransformId(slo.id, slo.revision); } - private buildGroupBy(slo: SLO, indicator: APMTransactionErrorRateIndicator) { + private buildGroupBy(slo: SLODefinition, 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, @@ -68,7 +68,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato return this.buildCommonGroupBy(slo, '@timestamp', extraGroupByFields); } - private buildSource(slo: SLO, indicator: APMTransactionErrorRateIndicator) { + private buildSource(slo: SLODefinition, indicator: APMTransactionErrorRateIndicator) { const queryFilter: estypes.QueryDslQueryContainer[] = [ { range: { @@ -137,7 +137,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato }; } - private buildAggregations(slo: SLO) { + private buildAggregations(slo: SLODefinition) { return { 'slo.numerator': { filter: { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts index bc71be9e78b61..c6bce6a0b3c19 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts @@ -11,20 +11,19 @@ import { histogramIndicatorSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; - -import { InvalidTransformError } from '../../errors'; -import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; import { + getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, - getSLOTransformId, } from '../../../common/constants'; -import { SLO } from '../../domain/models'; +import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; +import { SLODefinition } from '../../domain/models'; +import { InvalidTransformError } from '../../errors'; import { GetHistogramIndicatorAggregation } from '../aggregations'; export class HistogramTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLO): TransformPutTransformRequest { + public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { if (!histogramIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -41,11 +40,11 @@ export class HistogramTransformGenerator extends TransformGenerator { ); } - private buildTransformId(slo: SLO): string { + private buildTransformId(slo: SLODefinition): string { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLO, indicator: HistogramIndicator) { + private buildSource(slo: SLODefinition, indicator: HistogramIndicator) { return { index: parseIndex(indicator.params.index), runtime_mappings: this.buildCommonRuntimeMappings(slo), @@ -73,7 +72,7 @@ export class HistogramTransformGenerator extends TransformGenerator { }; } - private buildAggregations(slo: SLO, indicator: HistogramIndicator) { + private buildAggregations(slo: SLODefinition, indicator: HistogramIndicator) { const getHistogramIndicatorAggregations = new GetHistogramIndicatorAggregation(indicator); return { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts index 22ed8b9d40e46..821e6c3197925 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts @@ -16,10 +16,10 @@ import { SLO_INGEST_PIPELINE_NAME, getSLOTransformId, } from '../../../common/constants'; -import { KQLCustomIndicator, SLO } from '../../domain/models'; +import { KQLCustomIndicator, SLODefinition } from '../../domain/models'; export class KQLCustomTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLO): TransformPutTransformRequest { + public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { if (!kqlCustomIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -36,11 +36,11 @@ export class KQLCustomTransformGenerator extends TransformGenerator { ); } - private buildTransformId(slo: SLO): string { + private buildTransformId(slo: SLODefinition): string { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLO, indicator: KQLCustomIndicator) { + private buildSource(slo: SLODefinition, indicator: KQLCustomIndicator) { return { index: parseIndex(indicator.params.index), runtime_mappings: this.buildCommonRuntimeMappings(slo), @@ -68,7 +68,7 @@ export class KQLCustomTransformGenerator extends TransformGenerator { }; } - private buildAggregations(slo: SLO, indicator: KQLCustomIndicator) { + private buildAggregations(slo: SLODefinition, indicator: KQLCustomIndicator) { const numerator = getElasticsearchQueryOrThrow(indicator.params.good); const denominator = getElasticsearchQueryOrThrow(indicator.params.total); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts index b10d55fd1e54c..25dd36bd0b5d9 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts @@ -7,22 +7,21 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { metricCustomIndicatorSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; - -import { InvalidTransformError } from '../../errors'; -import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; import { + getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, - getSLOTransformId, } from '../../../common/constants'; -import { MetricCustomIndicator, SLO } from '../../domain/models'; +import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; +import { MetricCustomIndicator, SLODefinition } from '../../domain/models'; +import { InvalidTransformError } from '../../errors'; import { GetCustomMetricIndicatorAggregation } from '../aggregations'; export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; export class MetricCustomTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLO): TransformPutTransformRequest { + public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { if (!metricCustomIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -39,11 +38,11 @@ export class MetricCustomTransformGenerator extends TransformGenerator { ); } - private buildTransformId(slo: SLO): string { + private buildTransformId(slo: SLODefinition): string { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLO, indicator: MetricCustomIndicator) { + private buildSource(slo: SLODefinition, indicator: MetricCustomIndicator) { return { index: parseIndex(indicator.params.index), runtime_mappings: this.buildCommonRuntimeMappings(slo), @@ -71,7 +70,7 @@ export class MetricCustomTransformGenerator extends TransformGenerator { }; } - private buildAggregations(slo: SLO, indicator: MetricCustomIndicator) { + private buildAggregations(slo: SLODefinition, indicator: MetricCustomIndicator) { if (indicator.params.good.equation.match(INVALID_EQUATION_REGEX)) { throw new Error(`Invalid equation: ${indicator.params.good.equation}`); } diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts index 25ed021fdbaac..78d6da1eb5bca 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.test.ts @@ -6,7 +6,7 @@ */ import { ALL_VALUE } from '@kbn/slo-schema'; -import { SLO } from '../../domain/models'; +import { SLODefinition } from '../../domain/models'; import { createSLO, createSyntheticsAvailabilityIndicator } from '../fixtures/slo'; import { SyntheticsAvailabilityTransformGenerator } from './synthetics_availability'; import { SYNTHETICS_INDEX_PATTERN } from '../../../common/constants'; @@ -318,7 +318,7 @@ describe('Synthetics Availability Transform Generator', () => { ...indicator.params, tags, }, - } as SLO['indicator'], + } as SLODefinition['indicator'], }); const transform = generator.getTransformParams(slo, spaceId); @@ -348,7 +348,7 @@ describe('Synthetics Availability Transform Generator', () => { ...indicator.params, monitorIds, }, - } as SLO['indicator'], + } as SLODefinition['indicator'], }); const transform = generator.getTransformParams(slo, spaceId); @@ -378,7 +378,7 @@ describe('Synthetics Availability Transform Generator', () => { ...indicator.params, projects, }, - } as SLO['indicator'], + } as SLODefinition['indicator'], }); const transform = generator.getTransformParams(slo, spaceId); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts index 43055a3b9b32d..ca820f524d7ef 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/synthetics_availability.ts @@ -23,9 +23,9 @@ import { } from '../../../common/constants'; import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { InvalidTransformError } from '../../errors'; -import { SLO } from '../../domain/models'; +import { SLODefinition } from '../../domain/models'; export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest { + public getTransformParams(slo: SLODefinition, spaceId: string): TransformPutTransformRequest { if (!syntheticsAvailabilityIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -42,11 +42,11 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator ); } - private buildTransformId(slo: SLO): string { + private buildTransformId(slo: SLODefinition): string { return getSLOTransformId(slo.id, slo.revision); } - private buildGroupBy(slo: SLO, indicator: SyntheticsAvailabilityIndicator) { + private buildGroupBy(slo: SLODefinition, indicator: SyntheticsAvailabilityIndicator) { // These are the group by fields that will be used in `groupings` key // in the summary and rollup documents. For Synthetics, we want to use the // user-readible `monitor.name` and `observer.geo.name` fields by default, @@ -100,7 +100,11 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator ); } - private buildSource(slo: SLO, indicator: SyntheticsAvailabilityIndicator, spaceId: string) { + private buildSource( + slo: SLODefinition, + indicator: SyntheticsAvailabilityIndicator, + spaceId: string + ) { const queryFilter: estypes.QueryDslQueryContainer[] = [ { term: { 'summary.final_attempt': true } }, { term: { 'meta.space_id': spaceId } }, @@ -166,7 +170,7 @@ export class SyntheticsAvailabilityTransformGenerator extends TransformGenerator }; } - private buildAggregations(slo: SLO) { + private buildAggregations(slo: SLODefinition) { if (!occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) { throw new Error( "The sli.synthetics.availability indicator MUST have an 'Occurrences' budgeting method." diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts index 65140b08d6021..678dc2f4d76d8 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/timeslice_metric.ts @@ -21,13 +21,13 @@ import { SLO_INGEST_PIPELINE_NAME, getSLOTransformId, } from '../../../common/constants'; -import { SLO } from '../../domain/models'; +import { SLODefinition } from '../../domain/models'; import { GetTimesliceMetricIndicatorAggregation } from '../aggregations'; const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; export class TimesliceMetricTransformGenerator extends TransformGenerator { - public getTransformParams(slo: SLO): TransformPutTransformRequest { + public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { if (!timesliceMetricIndicatorSchema.is(slo.indicator)) { throw new InvalidTransformError(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -44,11 +44,11 @@ export class TimesliceMetricTransformGenerator extends TransformGenerator { ); } - private buildTransformId(slo: SLO): string { + private buildTransformId(slo: SLODefinition): string { return getSLOTransformId(slo.id, slo.revision); } - private buildSource(slo: SLO, indicator: TimesliceMetricIndicator) { + private buildSource(slo: SLODefinition, indicator: TimesliceMetricIndicator) { return { index: parseIndex(indicator.params.index), runtime_mappings: this.buildCommonRuntimeMappings(slo), @@ -76,7 +76,7 @@ export class TimesliceMetricTransformGenerator extends TransformGenerator { }; } - private buildAggregations(slo: SLO, indicator: TimesliceMetricIndicator) { + private buildAggregations(slo: SLODefinition, indicator: TimesliceMetricIndicator) { if (indicator.params.metric.equation.match(INVALID_EQUATION_REGEX)) { throw new Error(`Invalid equation: ${indicator.params.metric.equation}`); } diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts index a2b9735aab499..0a2226d98d55a 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/transform_generator.ts @@ -11,12 +11,15 @@ import { } 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'; +import { SLODefinition } from '../../domain/models'; export abstract class TransformGenerator { - public abstract getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest; + public abstract getTransformParams( + slo: SLODefinition, + spaceId: string + ): TransformPutTransformRequest; - public buildCommonRuntimeMappings(slo: SLO): MappingRuntimeFields { + public buildCommonRuntimeMappings(slo: SLODefinition): MappingRuntimeFields { return { 'slo.id': { type: 'keyword', @@ -33,12 +36,12 @@ export abstract class TransformGenerator { }; } - public buildDescription(slo: SLO): string { + public buildDescription(slo: SLODefinition): string { return `Rolled-up SLI data for SLO: ${slo.name} [id: ${slo.id}, revision: ${slo.revision}]`; } public buildCommonGroupBy( - slo: SLO, + slo: SLODefinition, sourceIndexTimestampField: string | undefined = '@timestamp', extraGroupByFields = {} ) { @@ -79,7 +82,7 @@ export abstract class TransformGenerator { } public buildSettings( - slo: SLO, + slo: SLODefinition, sourceIndexTimestampField: string | undefined = '@timestamp' ): TransformSettings { return { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts index 75ce8fcffc8e1..d7758f0fbdd4a 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.test.ts @@ -20,7 +20,7 @@ import { ApmTransactionErrorRateTransformGenerator, TransformGenerator, } from './transform_generators'; -import { SLO, IndicatorTypes } from '../domain/models'; +import { SLODefinition, IndicatorTypes } from '../domain/models'; import { createAPMTransactionDurationIndicator, createAPMTransactionErrorRateIndicator, @@ -189,13 +189,13 @@ describe('TransformManager', () => { }); class DummyTransformGenerator extends TransformGenerator { - getTransformParams(slo: SLO): TransformPutTransformRequest { + getTransformParams(slo: SLODefinition): TransformPutTransformRequest { return {} as TransformPutTransformRequest; } } class FailTransformGenerator extends TransformGenerator { - getTransformParams(slo: SLO): TransformPutTransformRequest { + getTransformParams(slo: SLODefinition): TransformPutTransformRequest { throw new Error('Some error'); } } diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts index b53b87837b533..e00d67e9944bb 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_manager.ts @@ -8,7 +8,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SLO, IndicatorTypes } from '../domain/models'; +import { SLODefinition, IndicatorTypes } from '../domain/models'; import { SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; import { TransformGenerator } from './transform_generators'; @@ -16,8 +16,8 @@ import { TransformGenerator } from './transform_generators'; type TransformId = string; export interface TransformManager { - install(slo: SLO): Promise; - inspect(slo: SLO): TransformPutTransformRequest; + install(slo: SLODefinition): Promise; + inspect(slo: SLODefinition): TransformPutTransformRequest; preview(transformId: TransformId): Promise; start(transformId: TransformId): Promise; stop(transformId: TransformId): Promise; @@ -32,7 +32,7 @@ export class DefaultTransformManager implements TransformManager { private spaceId: string ) {} - async install(slo: SLO): Promise { + async install(slo: SLODefinition): Promise { const generator = this.generators[slo.indicator.type]; if (!generator) { this.logger.error(`No transform generator found for indicator type [${slo.indicator.type}]`); @@ -56,7 +56,7 @@ export class DefaultTransformManager implements TransformManager { return transformParams.transform_id; } - inspect(slo: SLO): TransformPutTransformRequest { + inspect(slo: SLODefinition): TransformPutTransformRequest { const generator = this.generators[slo.indicator.type]; if (!generator) { this.logger.error(`No transform generator found for indicator type [${slo.indicator.type}]`); diff --git a/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/__snapshots__/remote_summary_doc_to_slo.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/__snapshots__/remote_summary_doc_to_slo.test.ts.snap new file mode 100644 index 0000000000000..153c007adcfa1 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/__snapshots__/remote_summary_doc_to_slo.test.ts.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FromRemoteSummaryDocToSlo with kibana < v8.14 fallbacks to dummy indicator params and dates 1`] = ` +Object { + "budgetingMethod": "timeslices", + "createdAt": 2024-01-01T00:00:00.000Z, + "description": "irrelevant", + "enabled": true, + "groupBy": "*", + "id": "irrelevant", + "indicator": Object { + "params": Object { + "good": "", + "index": "", + "timestampField": "", + "total": "", + }, + "type": "sli.kql.custom", + }, + "name": "irrelevant", + "objective": Object { + "target": 0.9999, + "timesliceTarget": 0.95, + "timesliceWindow": Duration { + "unit": "m", + "value": 5, + }, + }, + "revision": 1, + "settings": Object { + "frequency": Duration { + "unit": "m", + "value": 1, + }, + "syncDelay": Duration { + "unit": "m", + "value": 1, + }, + }, + "tags": Array [ + "prod", + ], + "timeWindow": Object { + "duration": Duration { + "unit": "d", + "value": 7, + }, + "type": "rolling", + }, + "updatedAt": 2024-01-01T00:00:00.000Z, + "version": 1, +} +`; + +exports[`FromRemoteSummaryDocToSlo with kibana >= v8.14 uses the stringified indicator params and dates 1`] = ` +Object { + "budgetingMethod": "occurrences", + "createdAt": 2024-02-01T00:00:00.000Z, + "description": "irrelevant", + "enabled": true, + "groupBy": "*", + "id": "irrelevant", + "indicator": Object { + "params": Object { + "good": "irrelevant", + "index": "irrelvant", + "timestampField": "irrelevant", + "total": "irrelevant", + }, + "type": "sli.kql.custom", + }, + "name": "irrelevant", + "objective": Object { + "target": 0.9999, + "timesliceTarget": undefined, + "timesliceWindow": undefined, + }, + "revision": 1, + "settings": Object { + "frequency": Duration { + "unit": "m", + "value": 1, + }, + "syncDelay": Duration { + "unit": "m", + "value": 1, + }, + }, + "tags": Array [ + "prod", + ], + "timeWindow": Object { + "duration": Duration { + "unit": "d", + "value": 7, + }, + "type": "rolling", + }, + "updatedAt": 2024-02-01T00:00:00.000Z, + "version": 1, +} +`; diff --git a/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.test.ts new file mode 100644 index 0000000000000..d32c42e59fa76 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { ALL_VALUE } from '@kbn/slo-schema'; +import { fromRemoteSummaryDocumentToSloDefinition } from './remote_summary_doc_to_slo'; + +describe('FromRemoteSummaryDocToSlo', () => { + let loggerMock: jest.Mocked; + beforeEach(() => { + loggerMock = loggingSystemMock.createLogger(); + }); + + describe('with kibana < v8.14', () => { + it('fallbacks to dummy indicator params and dates', () => { + const slo = fromRemoteSummaryDocumentToSloDefinition( + { + service: { + environment: null, + name: null, + }, + transaction: { + name: null, + type: null, + }, + slo: { + indicator: { + type: 'sli.kql.custom', + }, + timeWindow: { + duration: '7d', + type: 'rolling', + }, + groupBy: ALL_VALUE, + groupings: {}, + instanceId: ALL_VALUE, + name: 'irrelevant', + description: 'irrelevant', + id: 'irrelevant', + budgetingMethod: 'timeslices', + revision: 1, + objective: { + target: 0.9999, + timesliceTarget: 0.95, + timesliceWindow: '5m', + }, + tags: ['prod'], + }, + goodEvents: 0, + totalEvents: 0, + errorBudgetEstimated: false, + errorBudgetRemaining: 1, + errorBudgetConsumed: 0, + errorBudgetInitial: 1 - 0.9999, + sliValue: -1, + statusCode: 0, + status: 'NO_DATA', + isTempDoc: true, + spaceId: 'irrelevant', + }, + loggerMock + ); + + expect(slo).toMatchSnapshot(); + }); + }); + + describe('with kibana >= v8.14', () => { + it('uses the stringified indicator params and dates', () => { + const slo = fromRemoteSummaryDocumentToSloDefinition( + { + service: { + environment: null, + name: null, + }, + transaction: { + name: null, + type: null, + }, + slo: { + indicator: { + type: 'sli.kql.custom', + params: { + index: 'irrelvant', + good: 'irrelevant', + total: 'irrelevant', + timestampField: 'irrelevant', + }, // added in 8.14 + }, + timeWindow: { + duration: '7d', + type: 'rolling', + }, + groupBy: ALL_VALUE, + groupings: {}, + instanceId: ALL_VALUE, + name: 'irrelevant', + description: 'irrelevant', + id: 'irrelevant', + budgetingMethod: 'occurrences', + revision: 1, + objective: { + target: 0.9999, + }, + tags: ['prod'], + createdAt: '2024-02-01T00:00:00.000Z', // added in 8.14 + updatedAt: '2024-02-01T00:00:00.000Z', // added in 8.14 + }, + goodEvents: 0, + totalEvents: 0, + errorBudgetEstimated: false, + errorBudgetRemaining: 1, + errorBudgetConsumed: 0, + errorBudgetInitial: 1 - 0.9999, + sliValue: -1, + statusCode: 0, + status: 'NO_DATA', + isTempDoc: true, + spaceId: 'irrelevant', + kibanaUrl: 'http://kibana.com/base-path', // added in 8.14 + }, + loggerMock + ); + + expect(slo).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.ts new file mode 100644 index 0000000000000..d63194c871139 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/unsafe_federated/remote_summary_doc_to_slo.ts @@ -0,0 +1,171 @@ +/* + * 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/logging'; +import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; +import { Indicator, indicatorSchema, sloDefinitionSchema } from '@kbn/slo-schema'; +import { assertNever } from '@kbn/std'; +import { isLeft } from 'fp-ts/lib/Either'; +import { SLODefinition } from '../../domain/models'; +import { EsSummaryDocument } from '../summary_transform_generator/helpers/create_temp_summary'; + +export function fromRemoteSummaryDocumentToSloDefinition( + summaryDoc: EsSummaryDocument, + logger: Logger +): SLODefinition | undefined { + const res = sloDefinitionSchema.decode({ + id: summaryDoc.slo.id, + name: summaryDoc.slo.name, + description: summaryDoc.slo.description, + indicator: getIndicator(summaryDoc, logger), + timeWindow: summaryDoc.slo.timeWindow, + budgetingMethod: summaryDoc.slo.budgetingMethod, + objective: { + target: summaryDoc.slo.objective.target, + timesliceTarget: summaryDoc.slo.objective.timesliceTarget ?? undefined, + timesliceWindow: summaryDoc.slo.objective.timesliceWindow ?? undefined, + }, + settings: { syncDelay: '1m', frequency: '1m' }, + revision: summaryDoc.slo.revision, + enabled: true, + tags: summaryDoc.slo.tags, + createdAt: summaryDoc.slo.createdAt ?? '2024-01-01T00:00:00.000Z', // fallback prior 8.14 + updatedAt: summaryDoc.slo.updatedAt ?? '2024-01-01T00:00:00.000Z', // fallback prior 8.14 + groupBy: summaryDoc.slo.groupBy, + version: 1, + }); + + if (isLeft(res)) { + const errors = formatErrors(res.left); + logger.error(`Invalid remote stored summary SLO with id [${summaryDoc.slo.id}]`); + logger.error(errors.join('|')); + + return undefined; + } + + return res.right; +} + +/** + * Temporary documents priors to 8.14 don't have indicator.params, therefore we need to fallback to a dummy + */ +function getIndicator(summaryDoc: EsSummaryDocument, logger: Logger): Indicator { + const res = indicatorSchema.decode(summaryDoc.slo.indicator); + + if (isLeft(res)) { + const errors = formatErrors(res.left); + logger.info( + `Invalid indicator from remote summary SLO id [${summaryDoc.slo.id}] - Fallback on dummy indicator` + ); + logger.info(errors.join('|')); + + return getDummyIndicator(summaryDoc); + } + + return res.right; +} + +function getDummyIndicator(summaryDoc: EsSummaryDocument): Indicator { + const indicatorType = summaryDoc.slo.indicator.type; + let indicator: Indicator; + switch (indicatorType) { + case 'sli.kql.custom': + indicator = { + type: indicatorType, + params: { + index: '', + good: '', + total: '', + timestampField: '', + }, + }; + break; + case 'sli.apm.transactionDuration': + indicator = { + type: indicatorType, + params: { + environment: '', + service: '', + transactionType: '', + transactionName: '', + threshold: 0, + index: '', + }, + }; + break; + case 'sli.apm.transactionErrorRate': + indicator = { + type: indicatorType, + params: { + environment: '', + service: '', + transactionType: '', + transactionName: '', + index: '', + }, + }; + break; + case 'sli.metric.custom': + indicator = { + type: indicatorType, + params: { + index: '', + good: { metrics: [{ name: '', aggregation: 'sum', field: '' }], equation: '' }, + total: { metrics: [{ name: '', aggregation: 'sum', field: '' }], equation: '' }, + timestampField: '', + }, + }; + break; + case 'sli.metric.timeslice': + indicator = { + type: indicatorType, + params: { + index: '', + metric: { + metrics: [], + equation: '', + threshold: 0, + comparator: 'GT', + }, + timestampField: '', + }, + }; + break; + case 'sli.histogram.custom': + indicator = { + type: indicatorType, + params: { + index: '', + timestampField: '', + good: { field: '', aggregation: 'value_count' }, + total: { field: '', aggregation: 'value_count' }, + }, + }; + break; + case 'sli.synthetics.availability': + indicator = { + type: indicatorType, + params: { + projects: [], + tags: [], + monitorIds: [ + { + value: '*', + label: 'All', + }, + ], + index: 'synthetics-*', + filter: '', + }, + }; + break; + default: + assertNever(indicatorType); + } + + return indicator; +} diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts index 8e7e1d4f0bcfe..e520b952128fd 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts @@ -21,7 +21,7 @@ import { SLO_DESTINATION_INDEX_PATTERN, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, } from '../../common/constants'; -import { SLO } from '../domain/models'; +import { SLODefinition } from '../domain/models'; import { fiveMinute, oneMinute } from './fixtures/duration'; import { createAPMTransactionErrorRateIndicator, @@ -331,7 +331,7 @@ describe('UpdateSLO', () => { expect(mockEsClient.index).toHaveBeenCalled(); } - function expectDeletionOfOriginalSLOResources(originalSlo: SLO) { + function expectDeletionOfOriginalSLOResources(originalSlo: SLODefinition) { const transformId = getSLOTransformId(originalSlo.id, originalSlo.revision); expect(mockTransformManager.stop).toHaveBeenCalledWith(transformId); expect(mockTransformManager.uninstall).toHaveBeenCalledWith(transformId); diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts index 1aed6d99028d3..a55f4b2ce9fca 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts @@ -17,7 +17,7 @@ import { SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../common/constants'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; -import { SLO } from '../domain/models'; +import { SLODefinition } from '../domain/models'; import { validateSLO } from '../domain/services'; import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; @@ -37,7 +37,7 @@ export class UpdateSLO { public async execute(sloId: string, params: UpdateSLOParams): Promise { const originalSlo = await this.repository.findById(sloId); - let updatedSlo: SLO = Object.assign({}, originalSlo, params, { + let updatedSlo: SLODefinition = Object.assign({}, originalSlo, params, { groupBy: !!params.groupBy ? params.groupBy : originalSlo.groupBy, }); @@ -99,7 +99,7 @@ export class UpdateSLO { this.esClient.index({ index: SLO_SUMMARY_TEMP_INDEX_NAME, id: `slo-${updatedSlo.id}`, - document: createTempSummaryDocument(updatedSlo, this.spaceId), + document: createTempSummaryDocument(updatedSlo, this.spaceId, this.basePath), refresh: true, }), { logger: this.logger } @@ -129,7 +129,7 @@ export class UpdateSLO { return this.toResponse(updatedSlo); } - private async deleteOriginalSLO(originalSlo: SLO) { + private async deleteOriginalSLO(originalSlo: SLODefinition) { try { const originalRollupTransformId = getSLOTransformId(originalSlo.id, originalSlo.revision); await this.transformManager.stop(originalRollupTransformId); @@ -179,7 +179,7 @@ export class UpdateSLO { }); } - private toResponse(slo: SLO): UpdateSLOResponse { + private toResponse(slo: SLODefinition): UpdateSLOResponse { return updateSLOResponseSchema.encode(slo); } } diff --git a/x-pack/plugins/observability_solution/slo/server/services/utils/index.ts b/x-pack/plugins/observability_solution/slo/server/services/utils/index.ts index a50d2640b3195..490a3a04a9a39 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/utils/index.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/utils/index.ts @@ -23,7 +23,7 @@ export const getFlattenedGroupings = ({ groupings: Record; }): Groupings => { const groupByFields = groupBy ? [groupBy].flat() : []; - const hasGroupings = Object.keys(groupings || []).length; + const hasGroupings = Object.keys(groupings ?? {}).length; const formattedGroupings = hasGroupings ? groupByFields.reduce((acc, group) => { acc[group] = `${get(groupings, group)}`; diff --git a/x-pack/plugins/observability_solution/slo/server/utils/queries.ts b/x-pack/plugins/observability_solution/slo/server/utils/queries.ts index bdacad577838c..fa581df62e745 100644 --- a/x-pack/plugins/observability_solution/slo/server/utils/queries.ts +++ b/x-pack/plugins/observability_solution/slo/server/utils/queries.ts @@ -101,3 +101,7 @@ export async function typedSearch< ): Promise> { return (await esClient.search(params)) as unknown as ESSearchResponse; } + +export function createEsParams(params: T): T { + return params; +} diff --git a/x-pack/plugins/observability_solution/slo/tsconfig.json b/x-pack/plugins/observability_solution/slo/tsconfig.json index ff1a484c4cd90..97057cf4ab6c5 100644 --- a/x-pack/plugins/observability_solution/slo/tsconfig.json +++ b/x-pack/plugins/observability_solution/slo/tsconfig.json @@ -85,12 +85,15 @@ "@kbn/discover-plugin", "@kbn/field-formats-plugin", "@kbn/core-http-server", - "@kbn/presentation-publishing", "@kbn/test-jest-helpers", "@kbn/core-ui-settings-browser-mocks", "@kbn/core-i18n-browser-mocks", "@kbn/core-theme-browser-mocks", "@kbn/core-notifications-browser-mocks", - "@kbn/core-http-browser-mocks" + "@kbn/core-http-browser-mocks", + "@kbn/data-view-field-editor-plugin", + "@kbn/securitysolution-io-ts-utils", + "@kbn/core-elasticsearch-server-mocks", + "@kbn/presentation-publishing" ] }